Skip to content

Commit

Permalink
Add Shoes::FEATURES and Shoes::EXTENSIONS so a display service and an…
Browse files Browse the repository at this point in the history
… app can negotiate extra APIs on top of old Shoes
  • Loading branch information
noahgibbs committed Dec 6, 2023
1 parent 9e97b32 commit 2f2d014
Show file tree
Hide file tree
Showing 17 changed files with 203 additions and 35 deletions.
4 changes: 4 additions & 0 deletions examples/scarpe_ext.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Shoes.app(features: :scarpe) do
para "This text could be CSS-ified", html_attributes: { class: "button_css_class" }
button "OK"
end
5 changes: 4 additions & 1 deletion lacci/lib/shoes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,19 @@ class << self
# @param width [Integer] The new app window width
# @param height [Integer] The new app window height
# @param resizable [Boolean] Whether the app window should be resizeable
# @param features [Symbol,Array<Symbol>] Additional Shoes extensions requested by the app
# @return [void]
# @see Shoes::App#new
def app(
title: "Shoes!",
width: 480,
height: 420,
resizable: true,
features: [],
&app_code_body
)
app = Shoes::App.new(title:, width:, height:, resizable:, &app_code_body)
f = [features].flatten # Make sure this is a list, not a single symbol
app = Shoes::App.new(title:, width:, height:, resizable:, features: f, &app_code_body)
app.init
app.run
nil
Expand Down
20 changes: 19 additions & 1 deletion lacci/lib/shoes/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ class << self

attr_reader :document_root

shoes_styles :title, :width, :height, :resizable
shoes_styles :title, :width, :height, :resizable, :features

# This is defined to avoid the linkable-id check in the Shoes-style method_missing def'n
def features
@features
end

CUSTOM_EVENT_LOOP_TYPES = ["displaylib", "return", "wait"]

Expand All @@ -20,6 +25,7 @@ def initialize(
width: 480,
height: 420,
resizable: true,
features: [],
&app_code_body
)
log_init("Shoes::App")
Expand All @@ -34,6 +40,18 @@ def initialize(
@do_shutdown = false
@event_loop_type = "displaylib" # the default

@features = features

unknown_ext = features - Shoes::FEATURES - Shoes::EXTENSIONS
unsupported_features = unknown_ext & Shoes::KNOWN_FEATURES
unless unsupported_features.empty?
@log.error("Shoes app requires feature(s) not supported by this display service: #{unsupported_features.inspect}!")
raise Shoes::Errors::UnsupportedFeature, "Shoes app needs features: #{unsupported_features.inspect}"
end
unless unknown_ext.empty?
@log.warn("Shoes app requested unknown features #{unknown_ext.inspect}! Known: #{(Shoes::FEATURES + Shoes::EXTENSIONS).inspect}")
end

super

# The draw context tracks current settings like fill and stroke,
Expand Down
17 changes: 17 additions & 0 deletions lacci/lib/shoes/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,23 @@ def self.find_lib_dir

# Fonts currently loaded and available
FONTS = []

# Standard features available in this display service - see KNOWN_FEATURES.
# These may or may not require the Shoes.app requesting them per-app.
FEATURES = []

# Nonstandard extensions, e.g. Scarpe extensions, supported by this display lib.
# An application may have to request the extensions for them to be available so
# that a casual reader can see Shoes.app(features: :scarpe) and realize why
# there are nonstandard styles or drawables.
EXTENSIONS = []

# These are all known features supported by this version of Lacci.
# Features on this list are allowed to be in FEATURES. Anything else
# goes in EXTENSIONS and is nonstandard.
KNOWN_FEATURES = [
:html, # Supports .to_html on display objects, HTML classes on drawables, etc.
].freeze
end

# Access and assign the release constants
Expand Down
67 changes: 57 additions & 10 deletions lacci/lib/shoes/drawable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,26 +164,55 @@ def linkable_properties_hash
# If a block is passed to shoes_style, that's the validation for the property. It should
# convert a given value to a valid value for the property or throw an exception.
#
# If feature is non-nil, it's the feature that an app must request in order to see this
# property.
#
# @param name [String,Symbol] the style name
# @param feature [Symbol,NilClass] the feature that must be defined for an app to request this style, or nil
# @block if block is given, call it to map the given style value to a valid value, or raise an exception
def shoes_style(name, &validator)
def shoes_style(name, feature: nil, &validator)
name = name.to_s

return if linkable_properties_hash[name]

linkable_properties << { name: name, validator: }
linkable_properties << { name: name, validator:, feature: }
linkable_properties_hash[name] = true
end

# Add these names as Shoes styles with the given validator, if any
def shoes_styles(*names, &validator)
names.each { |n| shoes_style(n, &validator) }
# Add these names as Shoes styles with the given validator and feature, if any
def shoes_styles(*names, feature: nil, &validator)
names.each { |n| shoes_style(n, feature:, &validator) }
end

def shoes_style_names
parent_prop_names = self != Shoes::Drawable ? self.superclass.shoes_style_names : []
# Query what feature, if any, is required to use a specific shoes_style.
# If no specific feature is needed, nil will be returned.
def feature_for_shoes_style(style_name)
style_name = style_name.to_s
lp = linkable_properties.detect { |prop| prop[:name] == style_name }
return lp[:feature] if lp

# If we get to the top of the superclass tree and we didn't find it, it's not here
if self.class == ::Shoes::Drawable
raise Shoes::Errors::NoSuchStyleError, "Can't find information for style #{style_name.inspect}!"
end

parent_prop_names | linkable_properties.map { |prop| prop[:name] }
super
end

# Return a list of shoes_style names with the given features. If with_features is nil,
# return them with a list of features for the current Shoes::App. For the list of
# styles available with no features requested, pass nil to with_features.
def shoes_style_names(with_features: nil)
# No with_features given? Use the ones requested by this Shoes::App
with_features ||= Shoes::App.instance.features
parent_prop_names = self != Shoes::Drawable ? self.superclass.shoes_style_names(with_features:) : []

if with_features == :all
subclass_props = linkable_properties
else
subclass_props = linkable_properties.select { |prop| !prop[:feature] || with_features.include?(prop[:feature]) }
end
parent_prop_names | subclass_props.map { |prop| prop[:name] }
end

def shoes_style_hashes
Expand All @@ -206,6 +235,22 @@ def shoes_style_name?(name)
def initialize(*args, **kwargs)
log_init("Shoes::#{self.class.name}") unless @log

# First, get the list of allowed and disallowed styles for the given features
# and make sure no disallowed styles were given.

app_features = Shoes::App.instance.features
this_app_styles = self.class.shoes_style_names.map(&:to_sym)
not_this_app_styles = self.class.shoes_style_names(with_features: :all).map(&:to_sym) - this_app_styles

bad_styles = kwargs.keys & not_this_app_styles
unless bad_styles.empty?
features_needed = bad_styles.map { |s| self.class.feature_for_shoes_style(s) }.uniq
raise Shoes::Errors::UnsupportedFeature, "The style(s) #{bad_styles.inspect} are only defined for applications that request specific features: #{features_needed.inspect} (you requested #{app_features.inspect})!"
end

# Next, check positional arguments and make sure the correct number and type
# were passed and match positional args with style names.

supplied_args = kwargs.keys

req_args = self.class.required_init_args
Expand Down Expand Up @@ -237,6 +282,8 @@ def initialize(*args, **kwargs)
end
end

# Styles that were *not* passed should be set to defaults

default_styles = Shoes::Drawable.drawable_default_styles[self.class]
this_drawable_styles = self.class.shoes_style_names.map(&:to_sym)

Expand All @@ -246,7 +293,7 @@ def initialize(*args, **kwargs)
instance_variable_set("@#{key}", val)
end

# If we have a keyword arg for a style, set it normally.
# If we have a keyword arg for a style, set it as specified.
(this_drawable_styles & kwargs.keys).each do |key|
val = self.class.validate_as(key, kwargs[key])
instance_variable_set("@#{key}", val)
Expand Down Expand Up @@ -427,7 +474,7 @@ def method_missing(name, *args, **kwargs, &block)
prop_name = name_s[0..-2]
if self.class.shoes_style_name?(prop_name)
self.class.define_method(name) do |new_value|
raise(Shoes::Errors::NoLinkableIdError, "Trying to set Shoes styles in an object with no linkable ID! #{inspect}") unless linkable_id
raise(Shoes::Errors::NoLinkableIdError, "Trying to set Shoes styles in a #{self.class} with no linkable ID!") unless linkable_id

new_value = self.class.validate_as(prop_name, new_value)
instance_variable_set("@" + prop_name, new_value)
Expand Down
3 changes: 3 additions & 0 deletions lacci/lib/shoes/drawables/flow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Flow < Shoes::Slot
def initialize(width: "100%", height: nil, margin: nil, padding: nil, **options, &block)
super
@options = options
unless @options.empty?
STDERR.puts "FLOW OPTIONS: #{@options.inspect}"
end

# Create the display-side drawable *before* instance_eval, which will add child drawables with their display drawables
create_display_drawable
Expand Down
18 changes: 8 additions & 10 deletions lacci/lib/shoes/drawables/para.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ class Para < Shoes::Drawable
shoes_styles :text_items, :size, :font
shoes_style(:stroke) { |val| Shoes::Colors.to_rgb(val) }

shoes_style(:align) do |val|
unless ["left", "center", "right"].include?(val)
raise(Shoes::Errors::InvalidAttributeValueError, "Align must be one of left, center or right!")
end
end

Shoes::Drawable.drawable_default_styles[Shoes::Para][:size] = :para

shoes_events # No Para-specific events yet

# Initializes a new instance of the `Para` widget.
#
# @param args The text content of the paragraph.
# @param stroke [String, nil] The color of the text stroke.
# @param size [Symbol] The size of the paragraph text.
# @param font [String, nil] The font of the paragraph text.
# @param hidden [Boolean] Determines if the paragraph is initially hidden.
# @param html_attributes [Hash] Additional HTML attributes for the paragraph.
# @param kwargs [Hash] the various Shoes styles for this paragraph.
#
# @example
# Shoes.app do
Expand All @@ -31,18 +33,14 @@ class Para < Shoes::Drawable
#
# p.replace "On top we'll switch to ", strong("bold"), "!"
# end
def initialize(*args, stroke: nil, size: :para, font: nil, **html_attributes)
kwargs = { stroke:, size:, font:, **html_attributes }.compact

def initialize(*args, **kwargs)
# Don't pass text_children args to Drawable#initialize
super(*[], **kwargs)

# Text_children alternates strings and TextDrawables, so we can't just pass
# it as a Shoes style. It won't serialize.
update_text_children(args)

@html_attributes = html_attributes || {}

create_display_drawable
end

Expand Down
3 changes: 3 additions & 0 deletions lacci/lib/shoes/drawables/stack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ def initialize(width: nil, height: nil, margin: nil, padding: nil, scroll: false
margin_right: nil, **options, &block)

@options = options
unless @options.empty?
STDERR.puts "STACK OPTIONS: #{@options.inspect}"
end

super

Expand Down
2 changes: 2 additions & 0 deletions lacci/lib/shoes/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ class BadArgumentListError < Shoes::Error; end
class SingletonError < Shoes::Error; end

class MultipleShoesSpecRunsError < Shoes::Error; end

class UnsupportedFeature < Shoes::Error; end
end
10 changes: 7 additions & 3 deletions lacci/test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def run_test_niente_app(
timeout: 5.0,
class_name: self.class,
method_name: self.name,
expect_process_fail: false,
display_service: "niente"
)
with_tempfiles([
Expand All @@ -52,12 +53,15 @@ def run_test_niente_app(
)
end

if expect_process_fail
assert(false, "Expected app to fail but it succeeded!") if $?.success?
return
end

# Check if the process exited normally or crashed (segfault, failure, timeout)
unless $?.success?
assert(false, "Niente app failed with exit code: #{$?.exitstatus}")
assert(false, "App failed with exit code: #{$?.exitstatus}")
return
end


end
end
20 changes: 20 additions & 0 deletions lacci/test/test_lacci.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,24 @@ def test_too_few_positional_args
end
SHOES_SPEC
end

def test_unsupported_feature
run_test_niente_code(<<~SHOES_APP, app_test_code: <<~SHOES_SPEC, expect_process_fail: true)
Shoes.app(features: :html) do
para "Not supported by Niente, though."
end
SHOES_APP
assert true
SHOES_SPEC
end

def test_unknown_feature
run_test_niente_code(<<~SHOES_APP, app_test_code: <<~SHOES_SPEC)
Shoes.app(features: :squid) do
para "No such feature, though."
end
SHOES_APP
assert true
SHOES_SPEC
end
end
5 changes: 5 additions & 0 deletions lib/scarpe/wv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class Scarpe::Webview::Drawable < Shoes::Linkable
"Monaco",
)

Shoes::FEATURES.push(:html)
Shoes::EXTENSIONS.push(:scarpe)

require_relative "shoes_spec"
Shoes::Spec.instance = Scarpe::Test

Expand Down Expand Up @@ -100,3 +103,5 @@ class Scarpe::Webview::Drawable < Shoes::Linkable
require_relative "wv/check"
require_relative "wv/progress"
require_relative "wv/arrow"

require_relative "wv/scarpe_extensions"
8 changes: 8 additions & 0 deletions lib/scarpe/wv/scarpe_extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

# This may or may not stay. It's basically an example extension. It can be done
# better, and there's no reason it should be specific to button.
Shoes::Button.shoes_style :html_class, feature: :html

# We have a number of real Scarpe extensions that need to be properly marked as such
# and moved in here. Padding is a great example, as is html_attributes.
6 changes: 4 additions & 2 deletions scarpe-components/lib/scarpe/components/calzini/button.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
module Scarpe::Components::Calzini
def button_element(props)
HTML.render do |h|
h.button(
button_props = {
id: html_id,
onclick: handler_js_code("click"),
onmouseover: handler_js_code("hover"),
style: button_style(props),
class: props["html_class"],
title: props["tooltip"],
) do
}.compact
h.button(**button_props) do
props["text"]
end
end
Expand Down
8 changes: 6 additions & 2 deletions scarpe-components/test/calzini/test_calzini_button.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ def test_button_defaults
assert_equal %{<button id="elt-1" onclick="handle('click')" onmouseover="handle('hover')"></button>}, @calzini.render("button", {})
end

def test_button_all_properties_set
def test_button_with_html_class
assert_equal %{<button id="elt-1" onclick="handle('click')" onmouseover="handle('hover')" class="buttonish"></button>}, @calzini.render("button", { "html_class" => "buttonish" })
end

def test_button_all_standard_properties_set
props = {
"color" => "red",
"padding_top" => "4",
Expand All @@ -32,7 +36,7 @@ def test_button_all_properties_set
@calzini.render("button", props)
end

def test_button_all_properties_nil
def test_button_all_standard_properties_nil
props = {
"color" => nil,
"padding_top" => nil,
Expand Down
Loading

0 comments on commit 2f2d014

Please sign in to comment.