Class: Scarpe::WebviewWidget

Inherits:
Shoes::Linkable show all
Includes:
Shoes::Log
Defined in:
lib/scarpe/wv/widget.rb

Overview

The WebviewWidget parent class helps connect a Webview widget with its Shoes equivalent, render itself to the Webview DOM, handle Javascript events and generally keep things working in Webview.

Constant Summary

Constants included from Shoes::Log

Shoes::Log::DEFAULT_COMPONENT, Shoes::Log::DEFAULT_DEBUG_LOG_CONFIG, Shoes::Log::DEFAULT_LOG_CONFIG

Instance Attribute Summary collapse

Attributes inherited from Shoes::Linkable

#linkable_id

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shoes::Log

configure_logger, #log_init, logger

Methods inherited from Shoes::Linkable

#bind_shoes_event, #send_self_event, #send_shoes_event, #unsub_shoes_event

Constructor Details

#initialize(properties) ⇒ WebviewWidget

Set instance variables for the display properties of this widget. Bind Shoes events for changes of parent widget and changes of property values.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/scarpe/wv/widget.rb', line 39

def initialize(properties)
  log_init("WV::Widget")

  # Call method, which looks up the parent
  @shoes_linkable_id = properties["shoes_linkable_id"] || properties[:shoes_linkable_id]
  unless @shoes_linkable_id
    raise "Could not find property shoes_linkable_id in #{properties.inspect}!"
  end

  # Set the display properties
  properties.each do |k, v|
    next if k == "shoes_linkable_id"

    instance_variable_set("@" + k.to_s, v)
  end

  # The parent field is *almost* simple enough that a typed display property would handle it.
  bind_shoes_event(event_name: "parent", target: shoes_linkable_id) do |new_parent_id|
    display_parent = WebviewDisplayService.instance.query_display_widget_for(new_parent_id)
    if @parent != display_parent
      set_parent(display_parent)
    end
  end

  # When Shoes widgets change properties, we get a change notification here
  bind_shoes_event(event_name: "prop_change", target: shoes_linkable_id) do |prop_changes|
    prop_changes.each do |k, v|
      instance_variable_set("@" + k, v)
    end
    properties_changed(prop_changes)
  end

  bind_shoes_event(event_name: "destroy", target: shoes_linkable_id) do
    destroy_self
  end

  super(linkable_id: @shoes_linkable_id)
end

Instance Attribute Details

#childrenObject (readonly)

An array of WebviewWidget children (possibly empty) of this widget



35
36
37
# File 'lib/scarpe/wv/widget.rb', line 35

def children
  @children
end

#parentObject (readonly)

The WebviewWidget parent of this widget



32
33
34
# File 'lib/scarpe/wv/widget.rb', line 32

def parent
  @parent
end

#shoes_linkable_idObject (readonly)

The Shoes ID corresponding to the Shoes widget for this Webview widget



29
30
31
# File 'lib/scarpe/wv/widget.rb', line 29

def shoes_linkable_id
  @shoes_linkable_id
end

Class Method Details

.display_class_for(scarpe_class_name) ⇒ Object

Return the corresponding Webview class for a particular Shoes class name



12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/scarpe/wv/widget.rb', line 12

def display_class_for(scarpe_class_name)
  scarpe_class = Shoes.const_get(scarpe_class_name)
  unless scarpe_class.ancestors.include?(Shoes::Linkable)
    raise "Scarpe Webview can only get display classes for Shoes " +
      "linkable widgets, not #{scarpe_class_name.inspect}!"
  end

  klass = Scarpe.const_get("Webview" + scarpe_class_name.split("::")[-1])
  if klass.nil?
    raise "Couldn't find corresponding Scarpe Webview class for #{scarpe_class_name.inspect}!"
  end

  klass
end

Instance Method Details

#add_child(child) ⇒ Object (protected)

Do not call directly, use set_parent



127
128
129
130
131
132
133
# File 'lib/scarpe/wv/widget.rb', line 127

def add_child(child)
  @children ||= []
  @children << child

  # If we add a child, we should redraw ourselves
  needs_update!
end

#bind(event) { ... } ⇒ Object

This binds a Scarpe JS callback, handled via a single dispatch point in the app

Parameters:

  • event (String)

    the Scarpe widget event name

Yields:

  • the block to call when the event occurs



212
213
214
215
216
# File 'lib/scarpe/wv/widget.rb', line 212

def bind(event, &block)
  raise("Widget has no linkable_id! #{inspect}") unless linkable_id

  WebviewDisplayService.instance.app.bind("#{linkable_id}-#{event}", &block)
end

#destroy_selfScarpe::Promise

Removes the element from both the Ruby Widget tree and the HTML DOM. Return a promise for when that HTML change will be visible.

Returns:

  • (Scarpe::Promise)

    a promise that is fulfilled when the HTML change is complete



222
223
224
225
# File 'lib/scarpe/wv/widget.rb', line 222

def destroy_self
  @parent&.remove_child(self)
  html_element.remove
end

#handler_js_code(handler_function_name, *args) ⇒ String

Generate JS code to trigger a specific event name on this widget with the supplies arguments.

Parameters:

  • handler_function_name (String)

    the event name - @see #bind

  • args (Array)

    additional arguments that will be passed to the event in the generated JS

Returns:

  • (String)

    the generated JS code



243
244
245
246
247
248
# File 'lib/scarpe/wv/widget.rb', line 243

def handler_js_code(handler_function_name, *args)
  raise("Widget has no linkable_id! #{inspect}") unless linkable_id

  js_args = ["'#{linkable_id}-#{handler_function_name}'", *args].join(", ")
  "scarpeHandler(#{js_args})"
end

#html_elementScarpe::WebWrangler::ElementWrangler

This gets a mini-webview for just this element and its children, if any. It is normally called by the widget itself to do its DOM management.

Returns:



176
177
178
# File 'lib/scarpe/wv/widget.rb', line 176

def html_element
  @elt_wrangler ||= Scarpe::WebWrangler::ElementWrangler.new(html_id)
end

#html_idString

Get the object's HTML ID

Returns:

  • (String)

    the HTML ID



190
191
192
# File 'lib/scarpe/wv/widget.rb', line 190

def html_id
  object_id.to_s
end

#inspectObject

A shorter inspect text for prettier irb output



110
111
112
# File 'lib/scarpe/wv/widget.rb', line 110

def inspect
  "#<#{self.class}:#{self.object_id} @shoes_linkable_id=#{@shoes_linkable_id} @parent=#{@parent.inspect} @children=#{@children.inspect}>"
end

#needs_update!void

This method returns an undefined value.

Request a full redraw of all widgets.

It's really hard to do dirty-tracking here because the redraws are fully asynchronous. And so we can't easily cancel one "in flight," and we can't easily pick up the latest changes... And we probably don't want to, because we may be halfway through a batch.



234
235
236
# File 'lib/scarpe/wv/widget.rb', line 234

def needs_update!
  WebviewDisplayService.instance.app.request_redraw!
end

#promise_updateScarpe::Promise

Return a promise that guarantees all currently-requested changes have completed

Returns:

  • (Scarpe::Promise)

    a promise that will be fulfilled when all pending changes have finished



183
184
185
# File 'lib/scarpe/wv/widget.rb', line 183

def promise_update
  html_element.promise_update
end

#properties_changed(changes) ⇒ Object

Properties_changed will be called automatically when properties change. The widget should delete any changes from the Hash that it knows how to incrementally handle, and pass the rest to super. If any changes go entirely un-handled, a full redraw will be scheduled. This exists to be overridden by children watching for changes.

Parameters:

  • changes (Hash)

    a Hash of new values for properties that have changed



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/scarpe/wv/widget.rb', line 85

def properties_changed(changes)
  # If a widget does something really nonstandard with its html_id or element, it will
  # need to override to prevent this from happening. That's easy enough, though.
  if changes.key?("hidden")
    hidden = changes.delete("hidden")
    if hidden
      html_element.set_style("display", "none")
    else
      new_style = style # Get current display CSS property, which may vary by subclass
      disp = new_style[:display]
      html_element.set_style("display", disp || "block")
    end
  end

  needs_update! unless changes.empty?
end

#remove_child(child) ⇒ Object (protected)

Do not call directly, use set_parent



117
118
119
120
121
122
123
124
# File 'lib/scarpe/wv/widget.rb', line 117

def remove_child(child)
  @children ||= []
  unless @children.include?(child)
    @log.error("remove_child: no such child(#{child.inspect}) for"\
      " parent(#{parent.inspect})!")
  end
  @children.delete(child)
end

#rgb_to_hex(color) ⇒ Object (protected)

Convert an [r, g, b, a] array to an HTML hex color code Arrays support alpha. HTML hex does not. So premultiply.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/scarpe/wv/widget.rb', line 137

def rgb_to_hex(color)
  return color if color.nil?

  r, g, b, a = *color
  if r.is_a?(Float)
    a ||= 1.0
    r_float = r * a
    g_float = g * a
    b_float = b * a
  else
    a ||= 255
    a_float = (a / 255.0)
    r_float = (r.to_f / 255.0) * a_float
    g_float = (g.to_f / 255.0) * a_float
    b_float = (b.to_f / 255.0) * a_float
  end

  r_int = (r_float * 255.0).to_i.clamp(0, 255)
  g_int = (g_float * 255.0).to_i.clamp(0, 255)
  b_int = (b_float * 255.0).to_i.clamp(0, 255)

  "#%0.2X%0.2X%0.2X" % [r_int, g_int, b_int]
end

#set_parent(new_parent) ⇒ Object

Give this widget a new parent, including managing the appropriate child lists for parent widgets.



103
104
105
106
107
# File 'lib/scarpe/wv/widget.rb', line 103

def set_parent(new_parent)
  @parent&.remove_child(self)
  new_parent&.add_child(self)
  @parent = new_parent
end

#styleObject (protected)

CSS styles



162
163
164
165
166
167
168
# File 'lib/scarpe/wv/widget.rb', line 162

def style
  styles = {}
  if @hidden
    styles[:display] = "none"
  end
  styles
end

#to_htmlString

to_html is intended to get the HTML DOM rendering of this object and its children. Calling it should be side-effect-free and NOT update the webview.

Returns:

  • (String)

    the rendered HTML



198
199
200
201
202
203
204
205
206
# File 'lib/scarpe/wv/widget.rb', line 198

def to_html
  @children ||= []
  child_markup = @children.map(&:to_html).join
  if respond_to?(:element)
    element { child_markup }
  else
    child_markup
  end
end