Skip to content

Commit

Permalink
Wrap rendered drawables in an outer div so it can be replaced or remo…
Browse files Browse the repository at this point in the history
…ved fully. (#437)

Don't do this for TextDrawables - they're tiny.
Make Link and Span be TextDrawables, which they should have been anyway.
Default needs_update to just updating the single drawable, not the whole tree.
This is related to #419, but doesn't fully fix it.
  • Loading branch information
noahgibbs authored Nov 3, 2023
1 parent 4e08272 commit 9a134cc
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 31 deletions.
19 changes: 19 additions & 0 deletions lib/scarpe/evented_assertions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,25 @@ def assert_html(actual_html, expected_tag, **opts, &block)
assert_equal expected_html, actual_html
end

# Assert that `actual_html` includes `expected_tag` with `opts`.
# This uses Scarpe's HTML tag-based renderer to render the tag and options
# into text, and valides that the full HTML contains that tag.
#
# @see Scarpe::Components::HTML.render
#
# @param actual_html [String] the html to compare to
# @param expected_tag [String,Symbol] the HTML tag, used to send a method call
# @param opts keyword options passed to the tag method call
# @yield block passed to the tag method call.
# @return [void]
def assert_contains_html(actual_html, expected_tag, **opts, &block)
expected_html = Scarpe::Components::HTML.render do |h|
h.public_send(expected_tag, opts, &block)
end

assert_include actual_html, expected_html
end

def return_assertion_data
if !@assertions_failed.empty?
return_results(false, "Assertions failed", assertion_data_as_a_struct)
Expand Down
2 changes: 1 addition & 1 deletion lib/scarpe/wv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ class Scarpe::Webview::Drawable < Shoes::Linkable
require_relative "wv/edit_box"
require_relative "wv/edit_line"
require_relative "wv/list_box"
require_relative "wv/span"
require_relative "wv/shape"

require_relative "wv/text_drawable"
require_relative "wv/span"
require_relative "wv/link"
require_relative "wv/line"
require_relative "wv/rect"
Expand Down
50 changes: 35 additions & 15 deletions lib/scarpe/wv/drawable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,11 @@ def properties_changed(changes)
if changes.key?("hidden")
hidden = changes.delete("hidden")
if hidden
html_element.set_style("display", "none")
html_wrapper_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")
html_wrapper_element.set_style("display", disp || "block")
end
end

Expand Down Expand Up @@ -180,26 +180,40 @@ def style

public

# This gets a mini-webview for just this element and its children, if any.
# This gets an accessor for just this element's HTML ID.
# It is normally called by the drawable itself to do its DOM management.
# Some drawables don't use their html_id for their outermost element,
# and so we add an outer wrapping div to make sure that remove(),
# hidden() etc. affect every part of the drawable. This accessor
# does *not* necessarily affect the outermost elements.
#
# @return [Scarpe::WebWrangler::ElementWrangler] a DOM object manager
def html_element
@elt_wrangler ||= Scarpe::Webview::WebWrangler::ElementWrangler.new(html_id)
end

# This gets an accessor for just this element's outer wrapping div.
# This allows replacing the entire drawable, not just its "main" element.
#
# @return [Scarpe::WebWrangler::ElementWrangler] a DOM object manager
def html_wrapper_element
@elt_wrangler ||= Scarpe::Webview::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
# Doesn't matter what ElementWrangler we use -- they all return an update promise
# that includes all pending updates, no matter who they're for.
html_element.promise_update
end

# Get the object's HTML ID
#
# @return [String] the HTML ID
def html_id
object_id.to_s
@linkable_id.to_s
end

# to_html is intended to get the HTML DOM rendering of this object and its children.
Expand All @@ -209,11 +223,7 @@ def html_id
def to_html
@children ||= []
child_markup = @children.map(&:to_html).join
if respond_to?(:element)
element { child_markup }
else
child_markup
end
"<div id='#{html_id}-wrap'>" + element { child_markup } + "</div>"
end

# This binds a Scarpe JS callback, handled via a single dispatch point in the app
Expand All @@ -234,18 +244,28 @@ def bind(event, &block)
def destroy_self
@parent&.remove_child(self)
unsub_all_shoes_events
html_element.remove
html_wrapper_element.remove
end

# Request a full redraw of all drawables.
# Request a full redraw of the entire window, including the entire tree of
# drawables and the outer "empty page" frame.
#
# 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 full_window_redraw!
DisplayService.instance.app.request_redraw!
end

# Request a full redraw of this drawable, including all its children.
# Can be overridden in drawable subclasses if needed. An override would normally
# only be needed if re-rendering the element with the given html_id
# wasn't enough (and then remove would also need to be overridden.)
#
# This occurs by default if a property is changed and the drawable
# doesn't remove its change in property_changed.
#
# @return [void]
def needs_update!
DisplayService.instance.app.request_redraw!
html_wrapper_element.outer_html = to_html
end

# Generate JS code to trigger a specific event name on this drawable with the supplies arguments.
Expand Down
2 changes: 1 addition & 1 deletion lib/scarpe/wv/link.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Scarpe::Webview
class Link < Drawable
class Link < TextDrawable
def initialize(properties)
super

Expand Down
2 changes: 1 addition & 1 deletion lib/scarpe/wv/span.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Scarpe::Webview
class Span < Drawable
class Span < TextDrawable
SIZES = {
inscription: 10,
ins: 10,
Expand Down
4 changes: 4 additions & 0 deletions lib/scarpe/wv/text_drawable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

module Scarpe::Webview
class TextDrawable < Drawable
def to_html
# Do not render TextDrawables with individual wrapper divs.
element
end
end

class << self
Expand Down
10 changes: 9 additions & 1 deletion lib/scarpe/wv/web_wrangler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,15 @@ def inner_html=(new_html)
@webwrangler.dom_change("document.getElementById(\"" + html_id + "\").innerHTML = `" + new_html + "`; true")
end

# Update the JS DOM element's inner_html. The given Ruby value will be inspected and assigned.
# Update the JS DOM element's outer_html. The given Ruby value will be converted to string and assigned in backquotes.
#
# @param new_html [String] the new outer_html
# @return [Scarpe::Promise] a promise that will be fulfilled when the change is complete
def outer_html=(new_html)
@webwrangler.dom_change("document.getElementById(\"" + html_id + "\").outerHTML = `" + new_html + "`; true")
end

# Update the JS DOM element's attribute. The given Ruby value will be inspected and assigned.
#
# @param attribute [String] the attribute name
# @param value [String] the new attribute value
Expand Down
20 changes: 18 additions & 2 deletions scarpe-components/lib/scarpe/components/calzini.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,26 @@ module Scarpe::Components::Calzini
}.freeze
private_constant :SIZES

def render(drawable, properties = shoes_styles, &block)
send("#{drawable}_element", properties, &block)
# Render the Shoes drawable of type `drawable_name` with
# the given properties to HTML and return it. If the
# drawable type takes a block (e.g. Stack or Flow) then
# the block will be properly rendered.
#
# @param drawable_name [String] the drawable name like "alert", "button" or "rect"
# @param properties [Hash] a drawable-specific hash of property names to values
# @block the block which, when called, will return the contents for drawable types with contents
# @return [String] the rendered HTML
def render(drawable_name, properties = shoes_styles, &block)
send("#{drawable_name}_element", properties, &block)
end

# Return HTML for an empty page element, to be filled with HTML
# renderings of the DOM tree.
#
# The wrapper-wvroot element is where Scarpe will fill in the
# DOM element.
#
# @return [String] the rendered HTML for the empty page object.
def empty_page_element
<<~HTML
<html>
Expand Down
6 changes: 3 additions & 3 deletions test/test_edit_box.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_renders_textarea
on_heartbeat do
box = edit_box
html_id = box.display.html_id
assert_html edit_box.display.to_html, :textarea, id: html_id, oninput: "scarpeHandler('#{box.display.shoes_linkable_id}-change', this.value)" do
assert_contains_html edit_box.display.to_html, :textarea, id: html_id, oninput: "scarpeHandler('#{box.display.shoes_linkable_id}-change', this.value)" do
"Hello, World!"
end
Expand All @@ -35,7 +35,7 @@ def test_renders_textarea_no_change_cb_on_manual_replace
box.text = "Awwww yeah"
wait fully_updated
html_id = box.display.html_id
assert_html edit_box.display.to_html, :textarea, id: html_id, oninput: "scarpeHandler('#{box.display.shoes_linkable_id}-change', this.value)" do
assert_contains_html edit_box.display.to_html, :textarea, id: html_id, oninput: "scarpeHandler('#{box.display.shoes_linkable_id}-change', this.value)" do
"Awwww yeah"
end
# Shoes3 does *not* fire a change event when manually replacing text
Expand All @@ -55,7 +55,7 @@ def test_textarea_width
on_heartbeat do
box = edit_box
html_id = box.display.html_id
assert_html edit_box.display.to_html,
assert_contains_html edit_box.display.to_html,
:textarea,
id: html_id,
oninput: "scarpeHandler('#{box.display.shoes_linkable_id}-change', this.value)",
Expand Down
19 changes: 19 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,25 @@ def assert_html(actual_html, expected_tag, **opts, &block)

assert_equal expected_html, actual_html
end

# Assert that `actual_html` includes `expected_tag` with `opts`.
# This uses Scarpe's HTML tag-based renderer to render the tag and options
# into text, and valides that the full HTML contains that tag.
#
# @see Scarpe::Components::HTML.render
#
# @param actual_html [String] the html to compare to
# @param expected_tag [String,Symbol] the HTML tag, used to send a method call
# @param opts keyword options passed to the tag method call
# @yield block passed to the tag method call.
# @return [void]
def assert_contains_html(actual_html, expected_tag, **opts, &block)
expected_html = Scarpe::Components::HTML.render do |h|
h.public_send(expected_tag, opts, &block)
end

assert_includes actual_html, expected_html
end
end

class LoggedScarpeTest < ScarpeWebviewTest
Expand Down
13 changes: 6 additions & 7 deletions test/test_image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@ def teardown
def test_renders_image
img = Scarpe::Webview::Image.new(@default_properties)

assert_html img.to_html, :img, id: img.html_id, src: @url
assert_contains_html img.to_html, :img, id: img.html_id, src: @url
end

def test_renders_image_with_specified_size
width = 100
height = 50
img = Scarpe::Webview::Image.new(@default_properties.merge(width:, height:))

assert_html img.to_html, :img, id: img.html_id, src: @url, style: "width:#{width}px;height:#{height}px"
assert_contains_html img.to_html, :img, id: img.html_id, src: @url, style: "width:#{width}px;height:#{height}px"
end

def test_renders_image_with_specified_position
top = 1
left = 5
img = Scarpe::Webview::Image.new(@default_properties.merge(top:, left:))

assert_html img.to_html, :img, id: img.html_id, src: @url, style: "top:#{top}px;left:#{left}px;position:absolute"
assert_contains_html img.to_html, :img, id: img.html_id, src: @url, style: "top:#{top}px;left:#{left}px;position:absolute"
end

def test_renders_image_with_specified_size_and_position
Expand All @@ -45,7 +45,7 @@ def test_renders_image_with_specified_size_and_position
left = 5
img = Scarpe::Webview::Image.new(@default_properties.merge(width:, height:, top:, left:))

assert_html img.to_html,
assert_contains_html img.to_html,
:img,
id: img.html_id,
src: @url,
Expand All @@ -56,10 +56,9 @@ def test_renders_clickable_image
target_url = "http://github.com/schwad/scarpe"
img = Scarpe::Webview::Image.new(@default_properties.merge("click" => target_url))

assert_equal "<a id=\"#{img.html_id}\" href=\"#{target_url}\">"\
assert_includes img.to_html, "<a id=\"#{img.html_id}\" href=\"#{target_url}\">"\
"<img id=\"#{img.html_id}\" src=\"#{@url}\" />"\
"</a>",
img.to_html
"</a>"
end

def test_image_size
Expand Down

0 comments on commit 9a134cc

Please sign in to comment.