lib/scarpe/wv/widget.rb in scarpe-0.2.1 vs lib/scarpe/wv/widget.rb in scarpe-0.2.2

- old
+ new

@@ -1,16 +1,20 @@ # frozen_string_literal: true class Scarpe - class WebviewWidget < DisplayService::Linkable - include Scarpe::Log + # 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. + class WebviewWidget < Shoes::Linkable + include Shoes::Log class << self + # Return the corresponding Webview class for a particular Shoes class name def display_class_for(scarpe_class_name) - scarpe_class = Scarpe.const_get(scarpe_class_name) - unless scarpe_class.ancestors.include?(Scarpe::DisplayService::Linkable) - raise "Scarpe Webview can only get display classes for Scarpe " + + 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? @@ -19,14 +23,21 @@ klass end end + # The Shoes ID corresponding to the Shoes widget for this Webview widget attr_reader :shoes_linkable_id + + # The WebviewWidget parent of this widget attr_reader :parent + + # An array of WebviewWidget children (possibly empty) of this widget attr_reader :children + # Set instance variables for the display properties of this widget. Bind Shoes + # events for changes of parent widget and changes of property values. 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] @@ -62,21 +73,46 @@ end super(linkable_id: @shoes_linkable_id) end - # This exists to be overridden by children watching for changes + # 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. + # + # @param changes [Hash] a Hash of new values for properties that have changed 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 + # Give this widget a new parent, including managing the appropriate child lists for parent widgets. def set_parent(new_parent) @parent&.remove_child(self) new_parent&.add_child(self) @parent = new_parent end + # A shorter inspect text for prettier irb output + def inspect + "#<#{self.class}:#{self.object_id} @shoes_linkable_id=#{@shoes_linkable_id} @parent=#{@parent.inspect} @children=#{@children.inspect}>" + end + protected # Do not call directly, use set_parent def remove_child(child) @children ||= [] @@ -120,58 +156,91 @@ b_int = (b_float * 255.0).to_i.clamp(0, 255) "#%0.2X%0.2X%0.2X" % [r_int, g_int, b_int] end + # CSS styles + def style + styles = {} + if @hidden + styles[:display] = "none" + end + styles + end + public - # This gets a mini-webview for just this element and its children, if any + # 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. + # + # @return [Scarpe::WebWrangler::ElementWrangler] a DOM object manager def html_element - @elt_wrangler ||= WebviewDisplayService.instance.doc_root.get_element_wrangler(html_id) + @elt_wrangler ||= Scarpe::WebWrangler::ElementWrangler.new(html_id) end # Return a promise that guarantees all currently-requested changes have completed + # + # @return [Scarpe::Promise] a promise that will be fulfilled when all pending changes have finished def promise_update html_element.promise_update end + # Get the object's HTML ID + # + # @return [String] the HTML ID def html_id object_id.to_s end # 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. + # + # @return [String] the rendered HTML def to_html @children ||= [] child_markup = @children.map(&:to_html).join if respond_to?(:element) element { child_markup } else child_markup end end - # This binds a Scarpe JS callback, handled via a single dispatch point in the document root + # This binds a Scarpe JS callback, handled via a single dispatch point in the app + # + # @param event [String] the Scarpe widget event name + # @yield the block to call when the event occurs def bind(event, &block) raise("Widget has no linkable_id! #{inspect}") unless linkable_id - WebviewDisplayService.instance.doc_root.bind("#{linkable_id}-#{event}", &block) + WebviewDisplayService.instance.app.bind("#{linkable_id}-#{event}", &block) end # Removes the element from both the Ruby Widget tree and the HTML DOM. # Return a promise for when that HTML change will be visible. + # + # @return [Scarpe::Promise] a promise that is fulfilled when the HTML change is complete def destroy_self @parent&.remove_child(self) html_element.remove end + # 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. + # + # @return [void] def needs_update! - WebviewDisplayService.instance.doc_root.request_redraw! + WebviewDisplayService.instance.app.request_redraw! end + # Generate JS code to trigger a specific event name on this widget with the supplies arguments. + # + # @param handler_function_name [String] the event name - @see #bind + # @param args [Array] additional arguments that will be passed to the event in the generated JS + # @return [String] the generated JS code 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})"