Class: Zui::Application

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(components: DEFAULT_COMPONENTS, ui: nil, &definition) ⇒ Application

Returns a new instance of Application.

Raises:

  • (ArgumentError)


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

#componentsObject (readonly)

Returns the value of attribute components.



9
10
11
# File 'lib/zui/application.rb', line 9

def components
  @components
end

#stateObject (readonly)

Returns the value of attribute state.



9
10
11
# File 'lib/zui/application.rb', line 9

def state
  @state
end

#surface_optionsObject (readonly)

Returns the value of attribute surface_options.



9
10
11
# File 'lib/zui/application.rb', line 9

def surface_options
  @surface_options
end

#surfacesObject (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

Raises:

  • (ArgumentError)


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] = options.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

Raises:

  • (ArgumentError)


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

Raises:

  • (ArgumentError)


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
  message = JSON.parse(raw_line)
  if message.is_a?(Hash) && message["v"] == PROTOCOL_VERSION && message["type"] == "tick"
    @scheduler.tick
    return
  end
  validate_message!(message)
  dispatch_event(message)
rescue JSON::ParserError => exception
  emit_protocol_error("invalid_json", exception.message)
rescue ProtocolError => exception
  emit_protocol_error("invalid_message", exception.message)
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

Raises:

  • (ArgumentError)


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

#stopObject



149
150
151
152
153
# File 'lib/zui/application.rb', line 149

def stop
  @running = false
  @scheduler.stop
  self
end

#treeObject



121
# File 'lib/zui/application.rb', line 121

def tree = @surfaces.transform_values(&:to_h)