Skip to content

Basic app #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -57,5 +57,7 @@ build-iPhoneSimulator/

*.dll
*.so
*.exp
*.lib

.vscode
88 changes: 88 additions & 0 deletions .idea/workspace.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added .ruby-version
Binary file not shown.
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -6,20 +6,30 @@

#### Windows

I recommend to install Ruby via [Scoop](https://scoop.sh/), then
I recommend to install Ruby via [winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/):

`winget install RubyInstallerTeam.RubyWithDevKit.3.2`

The reason why you need the dev kit is that the event machine gem needs to be compiled on the target machine.

Then install the dependencies (you likely need to close all terminals first):

- `gem install ffi`
- `gem install eventmachine`
- `gem install rx`

`RUBY_DLL_PATH` must be set:
`RUBY_DLL_PATH` must be set, e.g.:

`$env:RUBY_DLL_PATH="C:\dev\xframes-ruby"`

For convenience, you may launch `main.bat` or `main.ps1` depending on whether you are using a regular command line or PowerShell.

#### Linux

- `sudo apt install ruby-full`
- `sudo gem install ffi`
- `sudo gem install eventmachine`
- `sudo gem install rx`

### Run the application

@@ -34,4 +44,3 @@ Windows 11
Raspberry Pi 5

![image](https://github.com/user-attachments/assets/190f8603-a6db-45c6-a5f0-cfd4dc1b87e2)

4 changes: 4 additions & 0 deletions main.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@echo off
set RUBY_DLL_PATH=%CD%
echo RUBY_DLL_PATH set to %RUBY_DLL_PATH%
ruby main.rb
3 changes: 3 additions & 0 deletions main.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
$env:RUBY_DLL_PATH = Get-Location
Write-Output "RUBY_DLL_PATH set to $env:RUBY_DLL_PATH"
ruby main.rb
151 changes: 13 additions & 138 deletions main.rb
Original file line number Diff line number Diff line change
@@ -1,63 +1,13 @@
require 'ffi'
require 'json'
require 'thread'
require 'concurrent-ruby'
require 'eventmachine'

ImGuiCol = {
Text: 0,
TextDisabled: 1,
WindowBg: 2,
ChildBg: 3,
PopupBg: 4,
Border: 5,
BorderShadow: 6,
FrameBg: 7,
FrameBgHovered: 8,
FrameBgActive: 9,
TitleBg: 10,
TitleBgActive: 11,
TitleBgCollapsed: 12,
MenuBarBg: 13,
ScrollbarBg: 14,
ScrollbarGrab: 15,
ScrollbarGrabHovered: 16,
ScrollbarGrabActive: 17,
CheckMark: 18,
SliderGrab: 19,
SliderGrabActive: 20,
Button: 21,
ButtonHovered: 22,
ButtonActive: 23,
Header: 24,
HeaderHovered: 25,
HeaderActive: 26,
Separator: 27,
SeparatorHovered: 28,
SeparatorActive: 29,
ResizeGrip: 30,
ResizeGripHovered: 31,
ResizeGripActive: 32,
Tab: 33,
TabHovered: 34,
TabActive: 35,
TabUnfocused: 36,
TabUnfocusedActive: 37,
PlotLines: 38,
PlotLinesHovered: 39,
PlotHistogram: 40,
PlotHistogramHovered: 41,
TableHeaderBg: 42,
TableBorderStrong: 43,
TableBorderLight: 44,
TableRowBg: 45,
TableRowBgAlt: 46,
TextSelectedBg: 47,
DragDropTarget: 48,
NavHighlight: 49,
NavWindowingHighlight: 50,
NavWindowingDimBg: 51,
ModalWindowDimBg: 52,
COUNT: 53
}
require_relative 'theme'
require_relative 'sampleapp'
require_relative 'services'
require_relative 'treetraversal'
require_relative 'xframes'

# Colors for theme generation
theme2Colors = {
@@ -144,90 +94,13 @@
font_defs_json = JSON.pretty_generate(defs: font_size_pairs)
theme_json = JSON.pretty_generate(theme2)

class Node
attr_accessor :id, :root

def initialize(id, root)
@type = 'node'
@id = id
@root = root
end

def to_json(*options)
{
type: @type,
id: @id,
root: @root
}.to_json(*options)
end
end

class UnformattedText
attr_accessor :id, :text

def initialize(id, text)
@type = 'unformatted-text'
@id = id
@text = text
end

def to_json(*options)
{
type: @type,
id: @id,
text: @text
}.to_json(*options)
end
end


module XFrames
extend FFI::Library
if RUBY_PLATFORM =~ /win32|mingw|cygwin/
ffi_lib './xframesshared.dll'
else
ffi_lib './libxframesshared.so'
end

# Define callback types
callback :OnInitCb, [:pointer], :void
callback :OnTextChangedCb, [:int, :string], :void
callback :OnComboChangedCb, [:int, :int], :void
callback :OnNumericValueChangedCb, [:int, :float], :void
callback :OnBooleanValueChangedCb, [:int, :int], :void
callback :OnMultipleNumericValuesChangedCb, [:int, :pointer, :int], :void
callback :OnClickCb, [:int], :void

attach_function :init, [
:string, # assetsBasePath
:string, # rawFontDefinitions
:string, # rawStyleOverrideDefinitions
:OnInitCb,
:OnTextChangedCb,
:OnComboChangedCb,
:OnNumericValueChangedCb,
:OnBooleanValueChangedCb,
:OnMultipleNumericValuesChangedCb,
:OnClickCb
], :void

attach_function :setElement, [:string], :void

attach_function :setChildren, [:int, :string], :void
end
service = WidgetRegistrationService.new
shadow_node_traversal_helper = ShadowNodeTraversalHelper.new(service)

on_init = FFI::Function.new(:void, []) do
puts "OnInit called!"

node = Node.new(0, true)
unformatted_text = UnformattedText.new(1, "Hello, world")
root = Root.new()

children_ids = [1]

XFrames.setElement(node.to_json)
XFrames.setElement(unformatted_text.to_json)

XFrames.setChildren(0, children_ids.to_json)
shadow_node_traversal_helper.traverse_tree(root)
end

on_text_changed = FFI::Function.new(:void, [:int, :string]) do |id, text|
@@ -252,6 +125,8 @@ module XFrames
end

on_click = FFI::Function.new(:void, [:int]) do |id|
service.dispatch_on_click_event(id)

puts "Button clicked: ID=#{id}"
end

115 changes: 115 additions & 0 deletions sampleapp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
require 'rx'
require_relative 'theme'
require_relative 'widgetnode'
require 'thread'
require 'concurrent-ruby'

Comment on lines +1 to +6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify the dependencies are properly installed.

Please ensure that all required gems are listed in the project's Gemfile:

gem 'rx'
gem 'concurrent-ruby'

Run this script to check the dependencies:


🏁 Script executed:

#!/bin/bash
# Description: Check if required gems are installed and their versions.

# Check if gems are installed
gem list | grep -E "^(rx|concurrent-ruby)"

# Check latest versions on RubyGems
curl -s https://rubygems.org/api/v1/versions/rx.json | jq '.[0].number'
curl -s https://rubygems.org/api/v1/versions/concurrent-ruby.json | jq '.[0].number'

Length of output: 245


Missing Dependency: "rx" gem Not Installed

• The dependency check confirms that "concurrent-ruby" (v1.3.5) is properly installed.
• However, the gem "rx" required by the file (via require 'rx') is missing from the installed gems list.
• Please ensure that the project's Gemfile includes gem 'rx' (expected latest version "0.0.3") and install it accordingly.

class TodoItem
attr_accessor :text, :done

def initialize(text, done)
@text = text
@done = done
end
end

class AppState
attr_accessor :todo_text, :todo_items

def initialize(todo_text, todo_items)
@todo_text = todo_text
@todo_items = todo_items
end
end

$sample_app_state = Rx::BehaviorSubject.new(AppState.new("", []))
Comment on lines +16 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Encapsulate application state within a service class.

Using a global variable ($sample_app_state) for state management is not recommended. Consider encapsulating the state within a service class:

-$sample_app_state = Rx::BehaviorSubject.new(AppState.new("", []))

+class TodoService
+  def self.instance
+    @instance ||= new
+  end
+
+  def initialize
+    @state = Rx::BehaviorSubject.new(AppState.new("", []))
+  end
+
+  def state
+    @state
+  end
+
+  private_class_method :new
+end

This provides better encapsulation and follows the Singleton pattern.


def on_click
promise = Concurrent::Promise.execute do
new_todo_item = TodoItem.new("New Todo", false)
current_state = $sample_app_state.value
new_state = AppState.new(
current_state.todo_text,
current_state.todo_items + [new_todo_item]
)
$sample_app_state.on_next(new_state)
end

promise.wait
end
Comment on lines +27 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve async handling and state management.

The current implementation has several issues:

  1. Direct access to global state
  2. Blocking UI with promise.wait
  3. Hardcoded "New Todo" text

Consider this improved implementation:

-def on_click
-  promise = Concurrent::Promise.execute do
-    new_todo_item = TodoItem.new("New Todo", false)
-    current_state = $sample_app_state.value
-    new_state = AppState.new(
-      current_state.todo_text,
-      current_state.todo_items + [new_todo_item]
-    )
-    $sample_app_state.on_next(new_state)
-  end
-
-  promise.wait
-end

+def on_click(text = nil)
+  Concurrent::Promise.execute do
+    service = TodoService.instance
+    current_state = service.state.value
+    new_todo_item = TodoItem.new(text || current_state.todo_text || "New Todo", false)
+    new_state = AppState.new(
+      "",  # Reset input after adding
+      current_state.todo_items + [new_todo_item]
+    )
+    service.state.on_next(new_state)
+  end
+end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def on_click
promise = Concurrent::Promise.execute do
new_todo_item = TodoItem.new("New Todo", false)
current_state = $sample_app_state.value
new_state = AppState.new(
current_state.todo_text,
current_state.todo_items + [new_todo_item]
)
$sample_app_state.on_next(new_state)
end
promise.wait
end
def on_click(text = nil)
Concurrent::Promise.execute do
service = TodoService.instance
current_state = service.state.value
new_todo_item = TodoItem.new(text || current_state.todo_text || "New Todo", false)
new_state = AppState.new(
"", # Reset input after adding
current_state.todo_items + [new_todo_item]
)
service.state.on_next(new_state)
end
end


$text_style = WidgetStyle.new(
style: WidgetStyleDef.new(
style_rules: StyleRules.new(
font: FontDef.new(name: "roboto-regular", size: 32)
)
)
)

$button_style = WidgetStyle.new(
style: WidgetStyleDef.new(
style_rules: StyleRules.new(
font: FontDef.new(name: "roboto-regular", size: 32)
),
layout: YogaStyle.new(
width: "50%",
padding: {Edge[:Vertical] => 10},
margin: {Edge[:Left] => 140}
)
)
)

class App < BaseComponent
def initialize
super({})

promise = Concurrent::Promise.execute do
$sample_app_state.subscribe do |latest_app_state|
puts "app state changed"

@props.on_next({
"todo_text" => latest_app_state.todo_text,
"todo_items" => latest_app_state.todo_items
})
end
end

promise.wait

@props.on_next({
"todo_text" => "",
"todo_items" => [TodoItem.new("New Todo", false)]
})
end

def render
children = [button("Add todo", Proc.new {
on_click()
}, $button_style)]

promise = Concurrent::Promise.execute do
@props.value["todo_items"].each do |todo_item|
text = "#{todo_item.text} (#{todo_item.done ? 'done' : 'to do'})."
children << unformatted_text(text, $text_style)
end
end

promise.wait

node(children)
end

def dispose
@app_state_subscription.dispose
end
end
Comment on lines +62 to +105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix state management and async rendering issues.

Several issues need attention:

  1. Direct access to global state
  2. Blocking promises in render method
  3. Subscription variable @app_state_subscription is never set

Apply these improvements:

 class App < BaseComponent
   def initialize
     super({})
 
-    promise = Concurrent::Promise.execute do
-      $sample_app_state.subscribe do |latest_app_state|
-        puts "app state changed"
+    service = TodoService.instance
+    @app_state_subscription = service.state.subscribe do |latest_app_state|
+      puts "app state changed"
 
-        @props.on_next({
-          "todo_text" => latest_app_state.todo_text,
-          "todo_items" => latest_app_state.todo_items
-        })
-      end
+      @props.on_next({
+        "todo_text" => latest_app_state.todo_text,
+        "todo_items" => latest_app_state.todo_items
+      })
     end
 
-    promise.wait
-
     @props.on_next({
       "todo_text" => "",
       "todo_items" => [TodoItem.new("New Todo", false)]
     })
   end
 
   def render
-    children = [button("Add todo", Proc.new {
-      on_click()
-    }, $button_style)]
+    children = [button("Add todo", -> { on_click() }, Theme::Styles.button)]
 
-    promise = Concurrent::Promise.execute do
-      @props.value["todo_items"].each do |todo_item|
-        text = "#{todo_item.text} (#{todo_item.done ? 'done' : 'to do'})."
-        children << unformatted_text(text, $text_style)
-      end
+    @props.value["todo_items"].each do |todo_item|
+      text = "#{todo_item.text} (#{todo_item.done ? 'done' : 'to do'})."
+      children << unformatted_text(text, Theme::Styles.text)
     end
 
-    promise.wait
-
     node(children)
   end
 
   def dispose
     @app_state_subscription.dispose
   end
 end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class App < BaseComponent
def initialize
super({})
promise = Concurrent::Promise.execute do
$sample_app_state.subscribe do |latest_app_state|
puts "app state changed"
@props.on_next({
"todo_text" => latest_app_state.todo_text,
"todo_items" => latest_app_state.todo_items
})
end
end
promise.wait
@props.on_next({
"todo_text" => "",
"todo_items" => [TodoItem.new("New Todo", false)]
})
end
def render
children = [button("Add todo", Proc.new {
on_click()
}, $button_style)]
promise = Concurrent::Promise.execute do
@props.value["todo_items"].each do |todo_item|
text = "#{todo_item.text} (#{todo_item.done ? 'done' : 'to do'})."
children << unformatted_text(text, $text_style)
end
end
promise.wait
node(children)
end
def dispose
@app_state_subscription.dispose
end
end
class App < BaseComponent
def initialize
super({})
service = TodoService.instance
@app_state_subscription = service.state.subscribe do |latest_app_state|
puts "app state changed"
@props.on_next({
"todo_text" => latest_app_state.todo_text,
"todo_items" => latest_app_state.todo_items
})
end
@props.on_next({
"todo_text" => "",
"todo_items" => [TodoItem.new("New Todo", false)]
})
end
def render
children = [button("Add todo", -> { on_click() }, Theme::Styles.button)]
@props.value["todo_items"].each do |todo_item|
text = "#{todo_item.text} (#{todo_item.done ? 'done' : 'to do'})."
children << unformatted_text(text, Theme::Styles.text)
end
node(children)
end
def dispose
@app_state_subscription.dispose
end
end


class Root < BaseComponent
def initialize
super({})
end

def render
root_node([App.new])
end
end
162 changes: 162 additions & 0 deletions services.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
require 'json'
require 'rx'
require 'thread'
require_relative 'xframes'

# $events_subject = Rx::ReplaySubject.new()

class WidgetRegistrationService
def initialize
@id_generator_lock = Mutex.new
@id_widget_registration_lock = Mutex.new
@id_event_registration_lock = Mutex.new


# @events_subject = Rx::ReplaySubject.new()
# @events_subject.debounce(0.001).subscribe(Proc.new { |fn| fn.call })
# promise = Concurrent::Promise.execute do
# $events_subject.subscribe do |cb|
# puts "yo"
# cb()
# end
# end
# promise.wait

@widget_registry = {}
@on_click_registry = Rx::BehaviorSubject.new({})

@last_widget_id = 0
@last_component_id = 0
end

def get_widget_by_id(widget_id)
@id_widget_registration_lock.synchronize do
@widget_registry[widget_id]
end
end

def register_widget(widget_id, widget)
@id_widget_registration_lock.synchronize do
@widget_registry[widget_id] = widget
end
end

def get_next_widget_id
@id_generator_lock.synchronize do
widget_id = @last_widget_id
@last_widget_id += 1
widget_id
end
end

def get_next_component_id
@id_generator_lock.synchronize do
component_id = @last_component_id
@last_component_id += 1
component_id
end
end

def register_on_click(widget_id, on_click)
@id_event_registration_lock.synchronize do
new_registry = @on_click_registry.value.dup
new_registry[widget_id] = on_click
@on_click_registry.on_next(new_registry)
end
end

def dispatch_on_click_event(widget_id)
promise = Concurrent::Promise.execute do
on_click = @on_click_registry.value[widget_id]

if on_click
# promise = Concurrent::Promise.execute do
on_click()
# $events_subject.on_next(on_click)
# end


else
puts "Widget with id #{widget_id} has no on_click handler"
end
end

promise.wait
end

def create_widget(widget)
widget_json = widget.to_hash.to_json
set_element(widget_json)
end

def patch_widget(widget_id, widget)
widget_json = widget.to_json
patch_element(widget_id, widget_json)
end

def link_children(widget_id, child_ids)
children_json = child_ids.to_json
set_children(widget_id, children_json)
end

def set_data(widget_id, data)
data_json = data.to_json
element_internal_op(widget_id, data_json)
end

def append_data(widget_id, data)
data_json = data.to_json
element_internal_op(widget_id, data_json)
end

def reset_data(widget_id)
data_json = "".to_json
element_internal_op(widget_id, data_json)
end

def append_data_to_plot_line(widget_id, x, y)
plot_data = { x: x, y: y }
element_internal_op(widget_id, plot_data.to_json)
end

def set_plot_line_axes_decimal_digits(widget_id, x, y)
axes_data = { x: x, y: y }
element_internal_op(widget_id, axes_data.to_json)
end

def append_text_to_clipped_multi_line_text_renderer(widget_id, text)
extern_append_text(widget_id, text)
end

def set_input_text_value(widget_id, value)
input_text_data = { value: value }
element_internal_op(widget_id, value)
end

def set_combo_selected_index(widget_id, index)
selected_index_data = { index: index }
element_internal_op(widget_id, selected_index_data.to_json)
end

private

def set_element(json_data)
XFrames.setElement(json_data)
end

def patch_element(widget_id, json_data)
# Implement patch logic if needed
end

def set_children(widget_id, json_data)
XFrames.setChildren(widget_id, json_data)
end

def element_internal_op(widget_id, json_data)
# Implement internal operation if needed
end

def extern_append_text(widget_id, text)
# Handle external append text logic
end
Comment on lines +147 to +161
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement missing private methods.

Several private methods are missing implementations:

  • patch_element
  • element_internal_op
  • extern_append_text

These missing implementations could lead to runtime errors.

Would you like me to help implement these methods or create issues to track their implementation?

end
490 changes: 490 additions & 0 deletions theme.rb

Large diffs are not rendered by default.

140 changes: 140 additions & 0 deletions treetraversal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
require 'rx'
require 'json'
require_relative 'widgetnode'
require_relative 'widgettypes'
require_relative 'services'

class ShadowNode
attr_accessor :id, :renderable, :current_props, :children, :props_change_subscription, :children_change_subscription

def initialize(id, renderable)
@id = id
@renderable = renderable
@current_props = {}
@children = []
@props_change_subscription = nil
@children_change_subscription = nil
end

def to_hash
{
"id" => @id,
"current_props" => @current_props,
"children" => @children.map(&:to_hash)
}
end

def get_linkable_children
out = []
@children.each do |child|
next if child.nil? || child.renderable.nil?

if child.renderable.is_a?(WidgetNode)
out << child
elsif !child.children.empty?
out.concat(child.get_linkable_children)
end
end
out
end
end

class ShadowNodeTraversalHelper
def initialize(widget_registration_service)
@widget_registration_service = widget_registration_service
end

def are_props_equal(props1, props2)
props1 == props2
end

def subscribe_to_props_helper(shadow_node)
if shadow_node.props_change_subscription
shadow_node.props_change_subscription.dispose
end

promise = Concurrent::Promise.execute do

if shadow_node.renderable.is_a?(BaseComponent)
component = shadow_node.renderable
shadow_node.props_change_subscription = component.props.pipe(
Rx::Operators.skip(1)
).subscribe { |new_props| handle_component_props_change(shadow_node, component, new_props) }
elsif shadow_node.renderable.is_a?(WidgetNode)
shadow_node.props_change_subscription = shadow_node.renderable.props.pipe(
Rx::Operators.skip(1)
).subscribe { |new_props| handle_widget_node_props_change(shadow_node, shadow_node.renderable, new_props) }
end

end

promise.wait
end

def handle_widget_node(widget)
if widget.type == WidgetTypes[:Button]
on_click = widget.props["on_click"]
if on_click
@widget_registration_service.register_on_click(widget.id, on_click)
else
puts "Button widget must have on_click prop"
end
end
end
Comment on lines +74 to +83
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace puts with proper error handling.

Using puts for error logging is not recommended in production code. Consider using a proper logging framework or raising an error.

Apply this diff to improve error handling:

  def handle_widget_node(widget)
    if widget.type == WidgetTypes[:Button]
      on_click = widget.props["on_click"]
      if on_click
        @widget_registration_service.register_on_click(widget.id, on_click)
      else
-       puts "Button widget must have on_click prop"
+       raise ArgumentError, "Button widget must have on_click prop"
      end
    end
  end


def handle_component_props_change(shadow_node, component, new_props)
return if are_props_equal(shadow_node.current_props, new_props)

shadow_child = component.render
shadow_node.children = [traverse_tree(shadow_child)]
shadow_node.current_props = new_props

linkable_children = shadow_node.get_linkable_children
@widget_registration_service.link_children(shadow_node.id, linkable_children.map(&:id))
end

def handle_widget_node_props_change(shadow_node, widget_node, new_props)
@widget_registration_service.create_widget(
WidgetNode.create_raw_childless_widget_node_with_id(shadow_node.id, widget_node)
)

shadow_children = widget_node.children.map { |child| traverse_tree(child) }
shadow_node.children = shadow_children
shadow_node.current_props = new_props

@widget_registration_service.link_children(shadow_node.id, shadow_node.children.map(&:id))
end

def traverse_tree(renderable)
if renderable.is_a?(BaseComponent)
rendered_child = renderable.render
shadow_child = traverse_tree(rendered_child)
id = @widget_registration_service.get_next_component_id
shadow_node = ShadowNode.new(id, renderable)
shadow_node.children = [shadow_child]
# shadow_node.current_props = renderable.props.value
subscribe_to_props_helper(shadow_node)
return shadow_node
elsif renderable.is_a?(WidgetNode)
id = @widget_registration_service.get_next_widget_id
raw_node = create_raw_childless_widget_node_with_id(id, renderable)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix undefined method error.

The method create_raw_childless_widget_node_with_id is called but not defined in this class.

Apply this diff to fix the method call:

-     raw_node = create_raw_childless_widget_node_with_id(id, renderable)
+     raw_node = WidgetNode.create_raw_childless_widget_node_with_id(id, renderable)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raw_node = create_raw_childless_widget_node_with_id(id, renderable)
raw_node = WidgetNode.create_raw_childless_widget_node_with_id(id, renderable)

handle_widget_node(raw_node)
@widget_registration_service.create_widget(raw_node)

shadow_node = ShadowNode.new(id, renderable)
shadow_node.children = renderable.children.value.map { |child| traverse_tree(child) }
shadow_node.current_props = renderable.props.value

linkable_children = shadow_node.get_linkable_children
if !linkable_children.empty?
@widget_registration_service.link_children(id, linkable_children.map(&:id))
end

subscribe_to_props_helper(shadow_node)

return shadow_node
else
raise 'Unrecognised renderable'
end
end
end
107 changes: 107 additions & 0 deletions widgetnode.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
require 'rx'
require 'json'
require_relative 'theme'
require_relative 'widgettypes'

class BaseComponent
def initialize(props)
@props = Rx::BehaviorSubject.new(props)
end

# Abstract method to be overridden
def render
raise NotImplementedError, 'You must implement the render method'
end
end

class WidgetNode
attr_accessor :type, :props, :children

def initialize(type, props = {}, children = [])
@type = type
@props = Rx::BehaviorSubject.new(props)
@children = Rx::BehaviorSubject.new(children)
end
end

class RawChildlessWidgetNodeWithId
attr_reader :id, :type, :props

def initialize(id, type, props = {})
@id = id
@type = type
@props = props
end

def to_hash
out = {
'id' => @id,
'type' => @type.to_s
}

@props.each do |key, value|
unless value.is_a?(Proc)
if (value.is_a?(WidgetStyleDef) || value.is_a?(NodeStyleDef))
out[key] = value.to_hash
else
out[key] = value
end
end
end

out
end
end

def widget_node_factory(widget_type, props = {}, children = [])
WidgetNode.new(widget_type, props, children)
end

def create_raw_childless_widget_node_with_id(id, node)
RawChildlessWidgetNodeWithId.new(id, node.type, node.props.value)
end

def init_props_with_style(style = nil)
props = {}

if style
props['style'] = style.style if style.style
props['activeStyle'] = style.active_style if style.active_style
props['hoverStyle'] = style.hover_style if style.hover_style
props['disabledStyle'] = style.disabled_style if style.disabled_style
end

props
end

def root_node(children, style = nil)
props = init_props_with_style(style)
props['root'] = true

widget_node_factory(WidgetTypes[:Node], props, children)
end

def node(children, style = nil)
props = init_props_with_style(style)
props['root'] = false

widget_node_factory(WidgetTypes[:Node], props, children)
end

def unformatted_text(text, style = nil)
props = init_props_with_style(style)
props['text'] = text

widget_node_factory(WidgetTypes[:UnformattedText], props, [])
end

def button(label, on_click = nil, style = nil)
raise TypeError, 'on_click must be a callable' if on_click && !on_click.is_a?(Proc)

props = init_props_with_style(style)
props['label'] = label
props['on_click'] = on_click if on_click

widget_node_factory(WidgetTypes[:Button], props, [])
end

5 changes: 5 additions & 0 deletions widgettypes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
WidgetTypes = {
Node: 'node',
UnformattedText: 'unformatted-text',
Button: 'di-button'
}
36 changes: 36 additions & 0 deletions xframes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require 'ffi'

module XFrames
extend FFI::Library
if RUBY_PLATFORM =~ /win32|mingw|cygwin/
ffi_lib './xframesshared.dll'
else
ffi_lib './libxframesshared.so'
end

# Define callback types
callback :OnInitCb, [:pointer], :void
callback :OnTextChangedCb, [:int, :string], :void
callback :OnComboChangedCb, [:int, :int], :void
callback :OnNumericValueChangedCb, [:int, :float], :void
callback :OnBooleanValueChangedCb, [:int, :int], :void
callback :OnMultipleNumericValuesChangedCb, [:int, :pointer, :int], :void
callback :OnClickCb, [:int], :void

attach_function :init, [
:string, # assetsBasePath
:string, # rawFontDefinitions
:string, # rawStyleOverrideDefinitions
:OnInitCb,
:OnTextChangedCb,
:OnComboChangedCb,
:OnNumericValueChangedCb,
:OnBooleanValueChangedCb,
:OnMultipleNumericValuesChangedCb,
:OnClickCb
], :void

attach_function :setElement, [:string], :void

attach_function :setChildren, [:int, :string], :void
end