Class: Zui::Application
- Inherits:
-
Object
- Object
- Zui::Application
- Defined in:
- lib/zui/application.rb
Constant Summary collapse
- ANIMATION_DEFAULTS =
{ "opacity" => 1.0, "scale" => 1.0, "rotation" => 0.0, "z" => 0.0 }.freeze
Instance Attribute Summary collapse
-
#components ⇒ Object
readonly
Returns the value of attribute components.
-
#state ⇒ Object
readonly
Returns the value of attribute state.
-
#surface_options ⇒ Object
readonly
Returns the value of attribute surface_options.
-
#surfaces ⇒ Object
readonly
Returns the value of attribute surfaces.
Instance Method Summary collapse
- #add_surface(name, node, options: {}) ⇒ Object
- #animate(node, properties, animation) ⇒ Object
- #animation_tracks(node, properties, animation) ⇒ Object
- #build_node(type, explicit_id: nil, props: {}) ⇒ Object
- #define_state(name, initial) ⇒ Object
- #emit_animation(node, tracks) ⇒ Object
- #emit_effect(name, payload = {}) ⇒ Object
-
#initialize(components: DEFAULT_COMPONENTS, ui: nil, &definition) ⇒ Application
constructor
A new instance of Application.
- #normalize_value(value, property = nil) ⇒ Object
- #receive(raw_line) ⇒ Object
- #register_binding(node, property, reader, animation: nil) ⇒ Object
- #register_handler(control_id, event, handler) ⇒ Object
- #register_structure(node, renderer) ⇒ Object
- #run(input: $stdin, output: $stdout, error: $stderr) ⇒ Object
- #schedule(kind, interval: 0, immediate: false, &block) ⇒ Object
- #start(output: $stdout, error: $stderr) ⇒ Object
- #stop ⇒ Object
- #tree ⇒ Object
Constructor Details
#initialize(components: DEFAULT_COMPONENTS, ui: nil, &definition) ⇒ Application
Returns a new instance of Application.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/zui/application.rb', line 11 def initialize(components: DEFAULT_COMPONENTS, ui: nil, &definition) @surfaces = {} @surface_options = {} @nodes = {} @bindings = [] @structures = [] @handlers = {} @sequence = 0 @output = nil @error = $stderr @running = false @patch_batch = nil @write_lock = Mutex.new @state_change_lock = Mutex.new @components = components.dup @builder = Builder.new(self) builder_extensions = Array(ui).compact unless builder_extensions.all? { |extension| extension.is_a?(Module) } raise ArgumentError, "ui extensions must be modules" end @builder.extend(*builder_extensions) unless builder_extensions.empty? @state = StateStore.new(method(:state_changed)) @scheduler = Scheduler.new(evaluator: method(:evaluate), on_error: method(:report_internal_error)) @builder.instance_eval(&definition) if definition raise ArgumentError, "plugin defines no surfaces" if @surfaces.empty? end |
Instance Attribute Details
#components ⇒ Object (readonly)
Returns the value of attribute components.
9 10 11 |
# File 'lib/zui/application.rb', line 9 def components @components end |
#state ⇒ Object (readonly)
Returns the value of attribute state.
9 10 11 |
# File 'lib/zui/application.rb', line 9 def state @state end |
#surface_options ⇒ Object (readonly)
Returns the value of attribute surface_options.
9 10 11 |
# File 'lib/zui/application.rb', line 9 def @surface_options end |
#surfaces ⇒ Object (readonly)
Returns the value of attribute surfaces.
9 10 11 |
# File 'lib/zui/application.rb', line 9 def surfaces @surfaces end |
Instance Method Details
#add_surface(name, node, options: {}) ⇒ Object
53 54 55 56 57 58 59 |
# File 'lib/zui/application.rb', line 53 def add_surface(name, node, options: {}) key = name.to_s validate_id!(key) raise ArgumentError, "duplicate surface: #{key}" if @surfaces.key?(key) @surfaces[key] = node @surface_options[key] = .transform_keys(&:to_s).transform_values { |value| normalize_value(value) } end |
#animate(node, properties, animation) ⇒ Object
93 94 95 |
# File 'lib/zui/application.rb', line 93 def animate(node, properties, animation) emit_animation(node, animation_tracks(node, properties, animation)) end |
#animation_tracks(node, properties, animation) ⇒ Object
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/zui/application.rb', line 97 def animation_tracks(node, properties, animation) definition = @components.fetch(node.type) properties.map do |property, value| property_name = property.to_s unless definition.properties.map(&:to_s).include?(property_name) raise ArgumentError, "unsupported animated property for #{node.type}: #{property_name}" end normalized = normalize_value(value, property_name) track = { "property" => property_name, "from" => node.props.fetch(property_name, ANIMATION_DEFAULTS[property_name]), "to" => normalized }.merge(animation.to_h) node.props[property_name] = normalized track end end |
#build_node(type, explicit_id: nil, props: {}) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/zui/application.rb', line 40 def build_node(type, explicit_id: nil, props: {}) definition = @components.fetch(type) @sequence += 1 id = explicit_id ? explicit_id.to_s : "#{type}.#{@sequence}" validate_id!(id) raise ArgumentError, "duplicate control id: #{id}" if @nodes.key?(id) normalized_props = normalize_props(props) unknown = normalized_props.keys - definition.properties.map(&:to_s) raise ArgumentError, "unsupported properties for #{type}: #{unknown.join(', ')}" unless unknown.empty? Node.new(type:, id:, props: normalized_props).tap { |node| @nodes[id] = node } end |
#define_state(name, initial) ⇒ Object
38 |
# File 'lib/zui/application.rb', line 38 def define_state(name, initial) = @state.define(name, initial) |
#emit_animation(node, tracks) ⇒ Object
115 116 117 118 119 |
# File 'lib/zui/application.rb', line 115 def emit_animation(node, tracks) raise ArgumentError, "animation requires at least one property" if tracks.empty? emit("v" => PROTOCOL_VERSION, "type" => "patch", "op" => "animate", "id" => node.id, "tracks" => tracks) node end |
#emit_effect(name, payload = {}) ⇒ Object
89 90 91 |
# File 'lib/zui/application.rb', line 89 def emit_effect(name, payload = {}) emit("v" => PROTOCOL_VERSION, "type" => "effect", "name" => name.to_s, "payload" => payload) end |
#normalize_value(value, property = nil) ⇒ Object
122 |
# File 'lib/zui/application.rb', line 122 def normalize_value(value, property = nil) = Value.normalize(value, property:) |
#receive(raw_line) ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/zui/application.rb', line 159 def receive(raw_line) raise ProtocolError, "message exceeds #{MAX_MESSAGE_BYTES} bytes" if raw_line.bytesize > MAX_MESSAGE_BYTES = JSON.parse(raw_line) if .is_a?(Hash) && ["v"] == PROTOCOL_VERSION && ["type"] == "tick" @scheduler.tick return end () dispatch_event() rescue JSON::ParserError => exception emit_protocol_error("invalid_json", exception.) rescue ProtocolError => exception emit_protocol_error("invalid_message", exception.) end |
#register_binding(node, property, reader, animation: nil) ⇒ Object
61 62 63 64 65 66 67 68 69 |
# File 'lib/zui/application.rb', line 61 def register_binding(node, property, reader, animation: nil) property = property.to_s unless @components.fetch(node.type).properties.map(&:to_s).include?(property) raise ArgumentError, "unsupported bound property for #{node.type}: #{property}" end value = normalize_value(evaluate(reader), property) node.props[property] = value @bindings << Binding.new(node:, property:, reader:, last_value: value, animation:) end |
#register_handler(control_id, event, handler) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/zui/application.rb', line 75 def register_handler(control_id, event, handler) raise ArgumentError, "handler requires a block" unless handler node = @nodes.fetch(control_id.to_s) { raise ArgumentError, "unknown event control: #{control_id}" } event_name = event.to_s declared = @components.fetch(node.type).events.map(&:to_s) unless declared.include?(event_name) || %w[mount unmount].include?(event_name) raise ArgumentError, "#{node.type} does not declare event: #{event_name}" end node.subscribe(event_name) key = [control_id.to_s, event_name] raise ArgumentError, "duplicate handler for #{key.join('/')}" if @handlers.key?(key) @handlers[key] = handler end |
#register_structure(node, renderer) ⇒ Object
71 72 73 |
# File 'lib/zui/application.rb', line 71 def register_structure(node, renderer) @structures << StructuralBinding.new(node:, renderer:, last_children: node.children.map(&:to_h)) end |
#run(input: $stdin, output: $stdout, error: $stderr) ⇒ Object
138 139 140 141 142 143 144 145 146 147 |
# File 'lib/zui/application.rb', line 138 def run(input: $stdin, output: $stdout, error: $stderr) start(output:, error:) input.each_line do |line| receive(line) rescue StandardError => exception report_internal_error(exception) end ensure stop end |
#schedule(kind, interval: 0, immediate: false, &block) ⇒ Object
155 156 157 |
# File 'lib/zui/application.rb', line 155 def schedule(kind, interval: 0, immediate: false, &block) @scheduler.schedule(kind, interval:, immediate:, &block) end |
#start(output: $stdout, error: $stderr) ⇒ Object
124 125 126 127 128 129 130 131 132 133 134 135 136 |
# File 'lib/zui/application.rb', line 124 def start(output: $stdout, error: $stderr) @output = output @error = error @output.sync = true if @output.respond_to?(:sync=) @error.sync = true if @error.respond_to?(:sync=) @running = true pid = Object.const_defined?(:Process) && Process.respond_to?(:pid) ? Process.pid : 0 emit("v" => PROTOCOL_VERSION, "type" => "ready", "pid" => pid, "surfaces" => @surfaces.keys) emit("v" => PROTOCOL_VERSION, "type" => "render", "components" => @components.protocol_schema, "surfaces" => tree, "surface_options" => @surface_options) @scheduler.start self end |
#stop ⇒ Object
149 150 151 152 153 |
# File 'lib/zui/application.rb', line 149 def stop @running = false @scheduler.stop self end |
#tree ⇒ Object
121 |
# File 'lib/zui/application.rb', line 121 def tree = @surfaces.transform_values(&:to_h) |