From 4f5fcd89a31e32b646cc9ecfbd14900950963ae8 Mon Sep 17 00:00:00 2001 From: Andrea Mancuso Date: Thu, 6 Feb 2025 20:25:14 +0100 Subject: [PATCH 1/9] Basic app --- .ruby-version | Bin 0 -> 16 bytes README.md | 2 + main.rb | 56 ------ theme.rb | 490 +++++++++++++++++++++++++++++++++++++++++++++++++ widgetnode.rb | 120 ++++++++++++ widgettypes.rb | 10 + 6 files changed, 622 insertions(+), 56 deletions(-) create mode 100644 .ruby-version create mode 100644 theme.rb create mode 100644 widgetnode.rb create mode 100644 widgettypes.rb diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000000000000000000000000000000000..0e8a452451658bf9ae97aa3cc7d6ec24d96e5e5d GIT binary patch literal 16 VcmezW&zM1v!32m68F(4E7yu}`0|@{C literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 9659eac..1e399f4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ I recommend to install Ruby via [Scoop](https://scoop.sh/), then - `gem install ffi` - `gem install eventmachine` +- `gem install rxruby` `RUBY_DLL_PATH` must be set: @@ -20,6 +21,7 @@ I recommend to install Ruby via [Scoop](https://scoop.sh/), then - `sudo apt install ruby-full` - `sudo gem install ffi` - `sudo gem install eventmachine` +- `sudo gem install rxruby` ### Run the application diff --git a/main.rb b/main.rb index ee7f62d..9099be0 100644 --- a/main.rb +++ b/main.rb @@ -2,62 +2,6 @@ require 'json' 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 -} # Colors for theme generation theme2Colors = { diff --git a/theme.rb b/theme.rb new file mode 100644 index 0000000..96592fe --- /dev/null +++ b/theme.rb @@ -0,0 +1,490 @@ + +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 +} + +ImPlotScale = { + Linear: 0, + Time: 1, + Log10: 2, + SymLog: 3 +} + +ImPlotMarker = { + None_: -1, + Circle: 0, + Square: 1, + Diamond: 2, + Up: 3, + Down: 4, + Left: 5, + Right: 6, + Cross: 7, + Plus: 8, + Asterisk: 9 +} + +ImGuiStyleVar = { + Alpha: 0, + DisabledAlpha: 1, + WindowPadding: 2, + WindowRounding: 3, + WindowBorderSize: 4, + WindowMinSize: 5, + WindowTitleAlign: 6, + ChildRounding: 7, + ChildBorderSize: 8, + PopupRounding: 9, + PopupBorderSize: 10, + FramePadding: 11, + FrameRounding: 12, + FrameBorderSize: 13, + ItemSpacing: 14, + ItemInnerSpacing: 15, + IndentSpacing: 16, + CellPadding: 17, + ScrollbarSize: 18, + ScrollbarRounding: 19, + GrabMinSize: 20, + GrabRounding: 21, + TabRounding: 22, + TabBorderSize: 23, + TabBarBorderSize: 24, + TableAngledHeadersAngle: 25, + TableAngledHeadersTextAlign: 26, + ButtonTextAlign: 27, + SelectableTextAlign: 28, + SeparatorTextBorderSize: 29, + SeparatorTextAlign: 30, + SeparatorTextPadding: 31 +} + +Align = { + Left: "left", + Right: "right" +} + +Direction = { + Inherit: "inherit", + Ltr: "ltr", + Rtl: "rtl" +} + +FlexDirection = { + Column: "column", + ColumnReverse: "column-reverse", + Row: "row", + RowReverse: "row-reverse" +} + +JustifyContent = { + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + SpaceBetween: "space-between", + SpaceAround: "space-around", + SpaceEvenly: "space-evenly" +} + +AlignContent = { + Auto: "auto", + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + Stretch: "stretch", + SpaceBetween: "space-between", + SpaceAround: "space-around", + SpaceEvenly: "space-evenly" +} + +AlignItems = { + Auto: "auto", + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + Stretch: "stretch", + Baseline: "baseline" +} + +AlignSelf = { + Auto: "auto", + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + Stretch: "stretch", + Baseline: "baseline" +} + +PositionType = { + Static: "static", + Relative: "relative", + Absolute: "absolute" +} + +FlexWrap = { + NoWrap: "no-wrap", + Wrap: "wrap", + WrapReverse: "wrap-reverse" +} + +Overflow = { + Visible: "visible", + Hidden: "hidden", + Scroll: "scroll" +} + +Display = { + Flex: "flex", + DisplayNone: "none" +} + +Edge = { + Left: "left", + Top: "top", + Right: "right", + Bottom: "bottom", + Start: "start", + End: "end", + Horizontal: "horizontal", + Vertical: "vertical", + All: "all" +} + +Gutter = { + Column: "column", + Row: "row", + All: "all" +} + +RoundCorners = { + All: "all", + TopLeft: "topLeft", + TopRight: "topRight", + BottomLeft: "bottomLeft", + BottomRight: "bottomRight" +} + +class FontDef + attr_accessor :name, :size + + def initialize(name:, size:) + @name = name + @size = size + end + + def to_hash + { "name" => @name, "size" => @size } + end +end + +class ImVec2 + attr_accessor :x, :y + + def initialize(x:, y:) + @x = x + @y = y + end + + def to_hash + { "x" => @x, "y" => @y } + end +end + + +class StyleRules + attr_accessor :align, :font, :colors, :vars + + def initialize(align: nil, font: nil, colors: nil, vars: nil) + @align = align + @font = font + @colors = colors + @vars = vars + end + + def to_hash + out = {} + + out[:align] = @align if @align + out[:font] = @font.to_hash if @font + out[:colors] = @colors.transform_keys(&:to_s) if @colors + out[:vars] = @vars.transform_keys(&:to_s) if @vars + + out + end +end + + +class BorderStyle + attr_reader :color, :thickness + + def initialize(color:, thickness: nil) + @color = color + @thickness = thickness + end + + def to_dict + out = { 'color' => @color } + + out['thickness'] = @thickness if @thickness + + out + end +end + + +class YogaStyle + attr_reader :direction, :flex_direction, :justify_content, :align_content, + :align_items, :align_self, :position_type, :flex_wrap, :overflow, + :display, :flex, :flex_grow, :flex_shrink, :flex_basis, :flex_basis_percent, + :position, :margin, :padding, :gap, :aspect_ratio, :width, + :min_width, :max_width, :height, :min_height, :max_height + + def initialize(direction: nil, flex_direction: nil, justify_content: nil, align_content: nil, + align_items: nil, align_self: nil, position_type: nil, flex_wrap: nil, overflow: nil, + display: nil, flex: nil, flex_grow: nil, flex_shrink: nil, flex_basis: nil, flex_basis_percent: nil, + position: nil, margin: nil, padding: nil, gap: nil, aspect_ratio: nil, width: nil, + min_width: nil, max_width: nil, height: nil, min_height: nil, max_height: nil) + @direction = direction + @flex_direction = flex_direction + @justify_content = justify_content + @align_content = align_content + @align_items = align_items + @align_self = align_self + @position_type = position_type + @flex_wrap = flex_wrap + @overflow = overflow + @display = display + @flex = flex + @flex_grow = flex_grow + @flex_shrink = flex_shrink + @flex_basis = flex_basis + @flex_basis_percent = flex_basis_percent + @position = position + @margin = margin + @padding = padding + @gap = gap + @aspect_ratio = aspect_ratio + @width = width + @min_width = min_width + @max_width = max_width + @height = height + @min_height = min_height + @max_height = max_height + end + + def to_dict + out = {} + + add_to_dict(out, 'direction', @direction) + add_to_dict(out, 'flexDirection', @flex_direction) + add_to_dict(out, 'justifyContent', @justify_content) + add_to_dict(out, 'alignContent', @align_content) + add_to_dict(out, 'alignItems', @align_items) + add_to_dict(out, 'alignSelf', @align_self) + add_to_dict(out, 'positionType', @position_type) + add_to_dict(out, 'flexWrap', @flex_wrap) + add_to_dict(out, 'overflow', @overflow) + add_to_dict(out, 'display', @display) + add_to_dict(out, 'flex', @flex) + add_to_dict(out, 'flexGrow', @flex_grow) + add_to_dict(out, 'flexShrink', @flex_shrink) + add_to_dict(out, 'flexBasis', @flex_basis) + add_to_dict(out, 'flexBasisPercent', @flex_basis_percent) + add_to_dict_with_edges(out, 'position', @position) + add_to_dict_with_edges(out, 'margin', @margin) + add_to_dict_with_edges(out, 'padding', @padding) + add_to_dict_with_gutters(out, 'gap', @gap) + add_to_dict(out, 'aspectRatio', @aspect_ratio) + add_to_dict(out, 'width', @width) + add_to_dict(out, 'minWidth', @min_width) + add_to_dict(out, 'maxWidth', @max_width) + add_to_dict(out, 'height', @height) + add_to_dict(out, 'minHeight', @min_height) + add_to_dict(out, 'maxHeight', @max_height) + + out + end + + private + + def add_to_dict(hash, key, value) + hash[key] = value unless value.nil? + end + + def add_to_dict_with_edges(hash, key, value) + return if value.nil? + + hash[key] = value.transform_keys(&:to_s) if value.is_a?(Hash) + end + + def add_to_dict_with_gutters(hash, key, value) + return if value.nil? + + hash[key] = value.transform_keys(&:to_s) if value.is_a?(Hash) + end +end + +class BaseDrawStyle + attr_accessor :background_color, :border, :border_top, :border_right, :border_bottom, :border_left, :rounding, :round_corners + + def initialize(background_color: nil, border: nil, border_top: nil, border_right: nil, border_bottom: nil, border_left: nil, rounding: nil, round_corners: nil) + @background_color = background_color + @border = border + @border_top = border_top + @border_right = border_right + @border_bottom = border_bottom + @border_left = border_left + @rounding = rounding + @round_corners = round_corners + end + + def to_hash + out = {} + + out['backgroundColor'] = @background_color if @background_color + out['border'] = @border.to_hash if @border + out['borderTop'] = @border_top.to_hash if @border_top + out['borderRight'] = @border_right.to_hash if @border_right + out['borderBottom'] = @border_bottom.to_hash if @border_bottom + out['borderLeft'] = @border_left.to_hash if @border_left + out['rounding'] = @rounding if @rounding + out['roundCorners'] = @round_corners if @round_corners + + out + end +end + +class NodeStyleDef + attr_accessor :layout, :base_draw + + def initialize(layout: nil, base_draw: nil) + @layout = layout + @base_draw = base_draw + end + + def to_hash + out = {} + + out.merge!(layout.to_hash) if layout + out.merge!(base_draw.to_hash) if base_draw + + out + end +end + +class WidgetStyleDef + attr_accessor :style_rules, :layout, :base_draw + + def initialize(style_rules: nil, layout: nil, base_draw: nil) + @style_rules = style_rules + @layout = layout + @base_draw = base_draw + end + + def to_hash + out = {} + + out.merge!(style_rules.to_hash) if style_rules + out.merge!(layout.to_hash) if layout + out.merge!(base_draw.to_hash) if base_draw + + out + end +end + +class NodeStyle + attr_accessor :style, :hover_style, :active_style, :disabled_style + + def initialize(style: nil, hover_style: nil, active_style: nil, disabled_style: nil) + @style = style + @hover_style = hover_style + @active_style = active_style + @disabled_style = disabled_style + end + + def to_hash + out = {} + + out[:style] = style.to_hash if style + out[:hover_style] = hover_style.to_hash if hover_style + out[:active_style] = active_style.to_hash if active_style + out[:disabled_style] = disabled_style.to_hash if disabled_style + + out + end +end + +class WidgetStyle + attr_accessor :style, :hover_style, :active_style, :disabled_style + + def initialize(style: nil, hover_style: nil, active_style: nil, disabled_style: nil) + @style = style + @hover_style = hover_style + @active_style = active_style + @disabled_style = disabled_style + end + + def to_hash + out = {} + + out[:style] = style.to_hash if style + out[:hover_style] = hover_style.to_hash if hover_style + out[:active_style] = active_style.to_hash if active_style + out[:disabled_style] = disabled_style.to_hash if disabled_style + + out + end +end diff --git a/widgetnode.rb b/widgetnode.rb new file mode 100644 index 0000000..cdd6374 --- /dev/null +++ b/widgetnode.rb @@ -0,0 +1,120 @@ +require 'rxruby' +require 'json' +require_relative 'theme' +require_relative 'widgettypes' + +class BaseComponent + include Rx::Reactive + + def initialize(props) + @props = 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 = BehaviorSubject.new(props) + @children = 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_serializable_hash + out = { + 'id' => @id, + 'type' => @type.to_s + } + + @props.each do |key, value| + out[key] = value unless value.is_a?(Proc) + 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 + +# JSON Encoder for RawChildlessWidgetNodeWithId +class RawChildlessWidgetNodeWithIdEncoder < JSON::Generator::GeneratorMethods::Object + def self.generate(obj) + case obj + when Enum + obj.value + when Proc + nil + when WidgetStyleDef + obj.to_dict + when NodeStyleDef + obj.to_dict + else + super(obj) + end + end +end diff --git a/widgettypes.rb b/widgettypes.rb new file mode 100644 index 0000000..d49c449 --- /dev/null +++ b/widgettypes.rb @@ -0,0 +1,10 @@ +class WidgetTypes + include JsonSerializableEnum + + JsonSerializableEnum.define_enum(self, + { name: :Component, string: 'component' }, + { name: :Node, string: 'node' }, + { name: :UnformattedText, string: 'unformatted-text' }, + { name: :Button, string: 'di-button' } + ) +end \ No newline at end of file From 0346c81a27721870352108cecc5bf0423266bc50 Mon Sep 17 00:00:00 2001 From: Andrea Mancuso Date: Thu, 6 Feb 2025 20:25:14 +0100 Subject: [PATCH 2/9] Basic app --- .ruby-version | Bin 0 -> 16 bytes README.md | 2 + main.rb | 56 ------ theme.rb | 490 +++++++++++++++++++++++++++++++++++++++++++++++++ widgetnode.rb | 120 ++++++++++++ widgettypes.rb | 10 + 6 files changed, 622 insertions(+), 56 deletions(-) create mode 100644 .ruby-version create mode 100644 theme.rb create mode 100644 widgetnode.rb create mode 100644 widgettypes.rb diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000000000000000000000000000000000..0e8a452451658bf9ae97aa3cc7d6ec24d96e5e5d GIT binary patch literal 16 VcmezW&zM1v!32m68F(4E7yu}`0|@{C literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 4e20eed..5b8d7a4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ I recommend to install Ruby via [Scoop](https://scoop.sh/), then - `gem install ffi` - `gem install eventmachine` +- `gem install rxruby` `RUBY_DLL_PATH` must be set: @@ -20,6 +21,7 @@ I recommend to install Ruby via [Scoop](https://scoop.sh/), then - `sudo apt install ruby-full` - `sudo gem install ffi` - `sudo gem install eventmachine` +- `sudo gem install rxruby` ### Run the application diff --git a/main.rb b/main.rb index ee7f62d..9099be0 100644 --- a/main.rb +++ b/main.rb @@ -2,62 +2,6 @@ require 'json' 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 -} # Colors for theme generation theme2Colors = { diff --git a/theme.rb b/theme.rb new file mode 100644 index 0000000..96592fe --- /dev/null +++ b/theme.rb @@ -0,0 +1,490 @@ + +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 +} + +ImPlotScale = { + Linear: 0, + Time: 1, + Log10: 2, + SymLog: 3 +} + +ImPlotMarker = { + None_: -1, + Circle: 0, + Square: 1, + Diamond: 2, + Up: 3, + Down: 4, + Left: 5, + Right: 6, + Cross: 7, + Plus: 8, + Asterisk: 9 +} + +ImGuiStyleVar = { + Alpha: 0, + DisabledAlpha: 1, + WindowPadding: 2, + WindowRounding: 3, + WindowBorderSize: 4, + WindowMinSize: 5, + WindowTitleAlign: 6, + ChildRounding: 7, + ChildBorderSize: 8, + PopupRounding: 9, + PopupBorderSize: 10, + FramePadding: 11, + FrameRounding: 12, + FrameBorderSize: 13, + ItemSpacing: 14, + ItemInnerSpacing: 15, + IndentSpacing: 16, + CellPadding: 17, + ScrollbarSize: 18, + ScrollbarRounding: 19, + GrabMinSize: 20, + GrabRounding: 21, + TabRounding: 22, + TabBorderSize: 23, + TabBarBorderSize: 24, + TableAngledHeadersAngle: 25, + TableAngledHeadersTextAlign: 26, + ButtonTextAlign: 27, + SelectableTextAlign: 28, + SeparatorTextBorderSize: 29, + SeparatorTextAlign: 30, + SeparatorTextPadding: 31 +} + +Align = { + Left: "left", + Right: "right" +} + +Direction = { + Inherit: "inherit", + Ltr: "ltr", + Rtl: "rtl" +} + +FlexDirection = { + Column: "column", + ColumnReverse: "column-reverse", + Row: "row", + RowReverse: "row-reverse" +} + +JustifyContent = { + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + SpaceBetween: "space-between", + SpaceAround: "space-around", + SpaceEvenly: "space-evenly" +} + +AlignContent = { + Auto: "auto", + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + Stretch: "stretch", + SpaceBetween: "space-between", + SpaceAround: "space-around", + SpaceEvenly: "space-evenly" +} + +AlignItems = { + Auto: "auto", + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + Stretch: "stretch", + Baseline: "baseline" +} + +AlignSelf = { + Auto: "auto", + FlexStart: "flex-start", + Center: "center", + FlexEnd: "flex-end", + Stretch: "stretch", + Baseline: "baseline" +} + +PositionType = { + Static: "static", + Relative: "relative", + Absolute: "absolute" +} + +FlexWrap = { + NoWrap: "no-wrap", + Wrap: "wrap", + WrapReverse: "wrap-reverse" +} + +Overflow = { + Visible: "visible", + Hidden: "hidden", + Scroll: "scroll" +} + +Display = { + Flex: "flex", + DisplayNone: "none" +} + +Edge = { + Left: "left", + Top: "top", + Right: "right", + Bottom: "bottom", + Start: "start", + End: "end", + Horizontal: "horizontal", + Vertical: "vertical", + All: "all" +} + +Gutter = { + Column: "column", + Row: "row", + All: "all" +} + +RoundCorners = { + All: "all", + TopLeft: "topLeft", + TopRight: "topRight", + BottomLeft: "bottomLeft", + BottomRight: "bottomRight" +} + +class FontDef + attr_accessor :name, :size + + def initialize(name:, size:) + @name = name + @size = size + end + + def to_hash + { "name" => @name, "size" => @size } + end +end + +class ImVec2 + attr_accessor :x, :y + + def initialize(x:, y:) + @x = x + @y = y + end + + def to_hash + { "x" => @x, "y" => @y } + end +end + + +class StyleRules + attr_accessor :align, :font, :colors, :vars + + def initialize(align: nil, font: nil, colors: nil, vars: nil) + @align = align + @font = font + @colors = colors + @vars = vars + end + + def to_hash + out = {} + + out[:align] = @align if @align + out[:font] = @font.to_hash if @font + out[:colors] = @colors.transform_keys(&:to_s) if @colors + out[:vars] = @vars.transform_keys(&:to_s) if @vars + + out + end +end + + +class BorderStyle + attr_reader :color, :thickness + + def initialize(color:, thickness: nil) + @color = color + @thickness = thickness + end + + def to_dict + out = { 'color' => @color } + + out['thickness'] = @thickness if @thickness + + out + end +end + + +class YogaStyle + attr_reader :direction, :flex_direction, :justify_content, :align_content, + :align_items, :align_self, :position_type, :flex_wrap, :overflow, + :display, :flex, :flex_grow, :flex_shrink, :flex_basis, :flex_basis_percent, + :position, :margin, :padding, :gap, :aspect_ratio, :width, + :min_width, :max_width, :height, :min_height, :max_height + + def initialize(direction: nil, flex_direction: nil, justify_content: nil, align_content: nil, + align_items: nil, align_self: nil, position_type: nil, flex_wrap: nil, overflow: nil, + display: nil, flex: nil, flex_grow: nil, flex_shrink: nil, flex_basis: nil, flex_basis_percent: nil, + position: nil, margin: nil, padding: nil, gap: nil, aspect_ratio: nil, width: nil, + min_width: nil, max_width: nil, height: nil, min_height: nil, max_height: nil) + @direction = direction + @flex_direction = flex_direction + @justify_content = justify_content + @align_content = align_content + @align_items = align_items + @align_self = align_self + @position_type = position_type + @flex_wrap = flex_wrap + @overflow = overflow + @display = display + @flex = flex + @flex_grow = flex_grow + @flex_shrink = flex_shrink + @flex_basis = flex_basis + @flex_basis_percent = flex_basis_percent + @position = position + @margin = margin + @padding = padding + @gap = gap + @aspect_ratio = aspect_ratio + @width = width + @min_width = min_width + @max_width = max_width + @height = height + @min_height = min_height + @max_height = max_height + end + + def to_dict + out = {} + + add_to_dict(out, 'direction', @direction) + add_to_dict(out, 'flexDirection', @flex_direction) + add_to_dict(out, 'justifyContent', @justify_content) + add_to_dict(out, 'alignContent', @align_content) + add_to_dict(out, 'alignItems', @align_items) + add_to_dict(out, 'alignSelf', @align_self) + add_to_dict(out, 'positionType', @position_type) + add_to_dict(out, 'flexWrap', @flex_wrap) + add_to_dict(out, 'overflow', @overflow) + add_to_dict(out, 'display', @display) + add_to_dict(out, 'flex', @flex) + add_to_dict(out, 'flexGrow', @flex_grow) + add_to_dict(out, 'flexShrink', @flex_shrink) + add_to_dict(out, 'flexBasis', @flex_basis) + add_to_dict(out, 'flexBasisPercent', @flex_basis_percent) + add_to_dict_with_edges(out, 'position', @position) + add_to_dict_with_edges(out, 'margin', @margin) + add_to_dict_with_edges(out, 'padding', @padding) + add_to_dict_with_gutters(out, 'gap', @gap) + add_to_dict(out, 'aspectRatio', @aspect_ratio) + add_to_dict(out, 'width', @width) + add_to_dict(out, 'minWidth', @min_width) + add_to_dict(out, 'maxWidth', @max_width) + add_to_dict(out, 'height', @height) + add_to_dict(out, 'minHeight', @min_height) + add_to_dict(out, 'maxHeight', @max_height) + + out + end + + private + + def add_to_dict(hash, key, value) + hash[key] = value unless value.nil? + end + + def add_to_dict_with_edges(hash, key, value) + return if value.nil? + + hash[key] = value.transform_keys(&:to_s) if value.is_a?(Hash) + end + + def add_to_dict_with_gutters(hash, key, value) + return if value.nil? + + hash[key] = value.transform_keys(&:to_s) if value.is_a?(Hash) + end +end + +class BaseDrawStyle + attr_accessor :background_color, :border, :border_top, :border_right, :border_bottom, :border_left, :rounding, :round_corners + + def initialize(background_color: nil, border: nil, border_top: nil, border_right: nil, border_bottom: nil, border_left: nil, rounding: nil, round_corners: nil) + @background_color = background_color + @border = border + @border_top = border_top + @border_right = border_right + @border_bottom = border_bottom + @border_left = border_left + @rounding = rounding + @round_corners = round_corners + end + + def to_hash + out = {} + + out['backgroundColor'] = @background_color if @background_color + out['border'] = @border.to_hash if @border + out['borderTop'] = @border_top.to_hash if @border_top + out['borderRight'] = @border_right.to_hash if @border_right + out['borderBottom'] = @border_bottom.to_hash if @border_bottom + out['borderLeft'] = @border_left.to_hash if @border_left + out['rounding'] = @rounding if @rounding + out['roundCorners'] = @round_corners if @round_corners + + out + end +end + +class NodeStyleDef + attr_accessor :layout, :base_draw + + def initialize(layout: nil, base_draw: nil) + @layout = layout + @base_draw = base_draw + end + + def to_hash + out = {} + + out.merge!(layout.to_hash) if layout + out.merge!(base_draw.to_hash) if base_draw + + out + end +end + +class WidgetStyleDef + attr_accessor :style_rules, :layout, :base_draw + + def initialize(style_rules: nil, layout: nil, base_draw: nil) + @style_rules = style_rules + @layout = layout + @base_draw = base_draw + end + + def to_hash + out = {} + + out.merge!(style_rules.to_hash) if style_rules + out.merge!(layout.to_hash) if layout + out.merge!(base_draw.to_hash) if base_draw + + out + end +end + +class NodeStyle + attr_accessor :style, :hover_style, :active_style, :disabled_style + + def initialize(style: nil, hover_style: nil, active_style: nil, disabled_style: nil) + @style = style + @hover_style = hover_style + @active_style = active_style + @disabled_style = disabled_style + end + + def to_hash + out = {} + + out[:style] = style.to_hash if style + out[:hover_style] = hover_style.to_hash if hover_style + out[:active_style] = active_style.to_hash if active_style + out[:disabled_style] = disabled_style.to_hash if disabled_style + + out + end +end + +class WidgetStyle + attr_accessor :style, :hover_style, :active_style, :disabled_style + + def initialize(style: nil, hover_style: nil, active_style: nil, disabled_style: nil) + @style = style + @hover_style = hover_style + @active_style = active_style + @disabled_style = disabled_style + end + + def to_hash + out = {} + + out[:style] = style.to_hash if style + out[:hover_style] = hover_style.to_hash if hover_style + out[:active_style] = active_style.to_hash if active_style + out[:disabled_style] = disabled_style.to_hash if disabled_style + + out + end +end diff --git a/widgetnode.rb b/widgetnode.rb new file mode 100644 index 0000000..cdd6374 --- /dev/null +++ b/widgetnode.rb @@ -0,0 +1,120 @@ +require 'rxruby' +require 'json' +require_relative 'theme' +require_relative 'widgettypes' + +class BaseComponent + include Rx::Reactive + + def initialize(props) + @props = 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 = BehaviorSubject.new(props) + @children = 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_serializable_hash + out = { + 'id' => @id, + 'type' => @type.to_s + } + + @props.each do |key, value| + out[key] = value unless value.is_a?(Proc) + 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 + +# JSON Encoder for RawChildlessWidgetNodeWithId +class RawChildlessWidgetNodeWithIdEncoder < JSON::Generator::GeneratorMethods::Object + def self.generate(obj) + case obj + when Enum + obj.value + when Proc + nil + when WidgetStyleDef + obj.to_dict + when NodeStyleDef + obj.to_dict + else + super(obj) + end + end +end diff --git a/widgettypes.rb b/widgettypes.rb new file mode 100644 index 0000000..d49c449 --- /dev/null +++ b/widgettypes.rb @@ -0,0 +1,10 @@ +class WidgetTypes + include JsonSerializableEnum + + JsonSerializableEnum.define_enum(self, + { name: :Component, string: 'component' }, + { name: :Node, string: 'node' }, + { name: :UnformattedText, string: 'unformatted-text' }, + { name: :Button, string: 'di-button' } + ) +end \ No newline at end of file From 57e0bfcd132ea11c145cfb4593f8ea02584de1d9 Mon Sep 17 00:00:00 2001 From: Andrea Mancuso Date: Thu, 6 Feb 2025 21:25:10 +0100 Subject: [PATCH 3/9] all methods named to_hash --- theme.rb | 62 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/theme.rb b/theme.rb index 96592fe..87eb681 100644 --- a/theme.rb +++ b/theme.rb @@ -275,7 +275,7 @@ def initialize(color:, thickness: nil) @thickness = thickness end - def to_dict + def to_hash out = { 'color' => @color } out['thickness'] = @thickness if @thickness @@ -325,52 +325,52 @@ def initialize(direction: nil, flex_direction: nil, justify_content: nil, align_ @max_height = max_height end - def to_dict + def to_hash out = {} - add_to_dict(out, 'direction', @direction) - add_to_dict(out, 'flexDirection', @flex_direction) - add_to_dict(out, 'justifyContent', @justify_content) - add_to_dict(out, 'alignContent', @align_content) - add_to_dict(out, 'alignItems', @align_items) - add_to_dict(out, 'alignSelf', @align_self) - add_to_dict(out, 'positionType', @position_type) - add_to_dict(out, 'flexWrap', @flex_wrap) - add_to_dict(out, 'overflow', @overflow) - add_to_dict(out, 'display', @display) - add_to_dict(out, 'flex', @flex) - add_to_dict(out, 'flexGrow', @flex_grow) - add_to_dict(out, 'flexShrink', @flex_shrink) - add_to_dict(out, 'flexBasis', @flex_basis) - add_to_dict(out, 'flexBasisPercent', @flex_basis_percent) - add_to_dict_with_edges(out, 'position', @position) - add_to_dict_with_edges(out, 'margin', @margin) - add_to_dict_with_edges(out, 'padding', @padding) - add_to_dict_with_gutters(out, 'gap', @gap) - add_to_dict(out, 'aspectRatio', @aspect_ratio) - add_to_dict(out, 'width', @width) - add_to_dict(out, 'minWidth', @min_width) - add_to_dict(out, 'maxWidth', @max_width) - add_to_dict(out, 'height', @height) - add_to_dict(out, 'minHeight', @min_height) - add_to_dict(out, 'maxHeight', @max_height) + add_to_hash(out, 'direction', @direction) + add_to_hash(out, 'flexDirection', @flex_direction) + add_to_hash(out, 'justifyContent', @justify_content) + add_to_hash(out, 'alignContent', @align_content) + add_to_hash(out, 'alignItems', @align_items) + add_to_hash(out, 'alignSelf', @align_self) + add_to_hash(out, 'positionType', @position_type) + add_to_hash(out, 'flexWrap', @flex_wrap) + add_to_hash(out, 'overflow', @overflow) + add_to_hash(out, 'display', @display) + add_to_hash(out, 'flex', @flex) + add_to_hash(out, 'flexGrow', @flex_grow) + add_to_hash(out, 'flexShrink', @flex_shrink) + add_to_hash(out, 'flexBasis', @flex_basis) + add_to_hash(out, 'flexBasisPercent', @flex_basis_percent) + add_to_hash_with_edges(out, 'position', @position) + add_to_hash_with_edges(out, 'margin', @margin) + add_to_hash_with_edges(out, 'padding', @padding) + add_to_hash_with_gutters(out, 'gap', @gap) + add_to_hash(out, 'aspectRatio', @aspect_ratio) + add_to_hash(out, 'width', @width) + add_to_hash(out, 'minWidth', @min_width) + add_to_hash(out, 'maxWidth', @max_width) + add_to_hash(out, 'height', @height) + add_to_hash(out, 'minHeight', @min_height) + add_to_hash(out, 'maxHeight', @max_height) out end private - def add_to_dict(hash, key, value) + def add_to_hash(hash, key, value) hash[key] = value unless value.nil? end - def add_to_dict_with_edges(hash, key, value) + def add_to_hash_with_edges(hash, key, value) return if value.nil? hash[key] = value.transform_keys(&:to_s) if value.is_a?(Hash) end - def add_to_dict_with_gutters(hash, key, value) + def add_to_hash_with_gutters(hash, key, value) return if value.nil? hash[key] = value.transform_keys(&:to_s) if value.is_a?(Hash) From 3076b40770d5c258183151b1c3d8c8065aaecfb7 Mon Sep 17 00:00:00 2001 From: Andrea Mancuso Date: Thu, 6 Feb 2025 22:23:12 +0100 Subject: [PATCH 4/9] No apparent cockups so far --- .idea/.gitignore | 8 +++ .idea/misc.xml | 4 ++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ .idea/xframes-ruby.iml | 15 +++++ main.rb | 40 ++--------- sampleapp.rb | 95 ++++++++++++++++++++++++++ services.rb | 147 +++++++++++++++++++++++++++++++++++++++++ treetraversal.rb | 132 ++++++++++++++++++++++++++++++++++++ widgetnode.rb | 27 ++------ widgettypes.rb | 15 ++--- xframes.rb | 36 ++++++++++ 12 files changed, 465 insertions(+), 68 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/xframes-ruby.iml create mode 100644 sampleapp.rb create mode 100644 services.rb create mode 100644 treetraversal.rb create mode 100644 xframes.rb diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..d959ea1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8d02977 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/xframes-ruby.iml b/.idea/xframes-ruby.iml new file mode 100644 index 0000000..6e7c09c --- /dev/null +++ b/.idea/xframes-ruby.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main.rb b/main.rb index 9099be0..01a1779 100644 --- a/main.rb +++ b/main.rb @@ -1,7 +1,11 @@ require 'ffi' require 'json' require 'eventmachine' - +require_relative 'theme' +require_relative 'sampleapp' +require_relative 'services' +require_relative 'treetraversal' +require_relative 'xframes' # Colors for theme generation theme2Colors = { @@ -125,40 +129,6 @@ def to_json(*options) 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 on_init = FFI::Function.new(:void, []) do puts "OnInit called!" diff --git a/sampleapp.rb b/sampleapp.rb new file mode 100644 index 0000000..0d7f8ee --- /dev/null +++ b/sampleapp.rb @@ -0,0 +1,95 @@ +require 'rx' +require_relative 'theme' +require_relative 'widgetnode' + +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("", [])) + +def on_click + 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 + +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({}) + + @app_state_subscription = sample_app_state.subscribe do |latest_app_state| + props.on_next({ + "todo_text" => latest_app_state.todo_text, + "todo_items" => latest_app_state.todo_items + }) + end + end + + def render + children = [button("Add todo", method(:on_click), button_style)] + + 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 + + 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 diff --git a/services.rb b/services.rb new file mode 100644 index 0000000..d67865f --- /dev/null +++ b/services.rb @@ -0,0 +1,147 @@ +require 'json' +require 'rx' +require 'thread' +require_relative 'xframes' + +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(10) + @events_subject.debounce(0.001).subscribe { |fn| fn.call } + + @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) + on_click = @on_click_registry.value[widget_id] + if on_click + @events_subject.on_next(on_click) + else + puts "Widget with id #{widget_id} has no on_click handler" + end + end + + def create_widget(widget) + widget_json = widget.to_serializable_dict.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 reset_data(widget_id, data) + data_json = data.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 +end diff --git a/treetraversal.rb b/treetraversal.rb new file mode 100644 index 0000000..d3e0b21 --- /dev/null +++ b/treetraversal.rb @@ -0,0 +1,132 @@ +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_dict + { + "id" => @id, + "current_props" => @current_props, + "children" => @children.map(&:to_dict) + } + 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 + + 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 + + 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 + + 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) + shadow_child = traverse_tree(renderable.render) + 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 = 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 diff --git a/widgetnode.rb b/widgetnode.rb index cdd6374..6007c57 100644 --- a/widgetnode.rb +++ b/widgetnode.rb @@ -1,13 +1,11 @@ -require 'rxruby' +require 'rx' require 'json' require_relative 'theme' require_relative 'widgettypes' class BaseComponent - include Rx::Reactive - def initialize(props) - @props = BehaviorSubject.new(props) + @props = Rx::BehaviorSubject.new(props) end # Abstract method to be overridden @@ -21,8 +19,8 @@ class WidgetNode def initialize(type, props = {}, children = []) @type = type - @props = BehaviorSubject.new(props) - @children = BehaviorSubject.new(children) + @props = Rx::BehaviorSubject.new(props) + @children = Rx::BehaviorSubject.new(children) end end @@ -101,20 +99,3 @@ def button(label, on_click = nil, style = nil) widget_node_factory(WidgetTypes::Button, props, []) end -# JSON Encoder for RawChildlessWidgetNodeWithId -class RawChildlessWidgetNodeWithIdEncoder < JSON::Generator::GeneratorMethods::Object - def self.generate(obj) - case obj - when Enum - obj.value - when Proc - nil - when WidgetStyleDef - obj.to_dict - when NodeStyleDef - obj.to_dict - else - super(obj) - end - end -end diff --git a/widgettypes.rb b/widgettypes.rb index d49c449..303e61f 100644 --- a/widgettypes.rb +++ b/widgettypes.rb @@ -1,10 +1,5 @@ -class WidgetTypes - include JsonSerializableEnum - - JsonSerializableEnum.define_enum(self, - { name: :Component, string: 'component' }, - { name: :Node, string: 'node' }, - { name: :UnformattedText, string: 'unformatted-text' }, - { name: :Button, string: 'di-button' } - ) -end \ No newline at end of file +WidgetTypes = { + Node: 'node', + UnformattedText: 'unformatted-text', + Button: 'di-button' +} \ No newline at end of file diff --git a/xframes.rb b/xframes.rb new file mode 100644 index 0000000..270b403 --- /dev/null +++ b/xframes.rb @@ -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 \ No newline at end of file From 571afb3b6ec23b0ee110f590401e1f10ab5ed06f Mon Sep 17 00:00:00 2001 From: Andrea Mancuso Date: Thu, 6 Feb 2025 23:01:37 +0100 Subject: [PATCH 5/9] Removed duplicate method --- services.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/services.rb b/services.rb index d67865f..713e01a 100644 --- a/services.rb +++ b/services.rb @@ -94,11 +94,6 @@ def reset_data(widget_id) element_internal_op(widget_id, data_json) end - def reset_data(widget_id, data) - data_json = data.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) From 26f2ed0d8a700f8ba9909b23ea0e1a313c704a76 Mon Sep 17 00:00:00 2001 From: "andrea.mancuso" Date: Fri, 7 Feb 2025 13:18:24 +0100 Subject: [PATCH 6/9] Update .gitignore and README.md --- .gitignore | 2 ++ README.md | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2a32391..0889a59 100644 --- a/.gitignore +++ b/.gitignore @@ -57,5 +57,7 @@ build-iPhoneSimulator/ *.dll *.so +*.exp +*.lib .vscode diff --git a/README.md b/README.md index 5b8d7a4..87bf8f5 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,17 @@ #### 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 rxruby` +- `gem install rx` `RUBY_DLL_PATH` must be set: @@ -21,7 +27,7 @@ I recommend to install Ruby via [Scoop](https://scoop.sh/), then - `sudo apt install ruby-full` - `sudo gem install ffi` - `sudo gem install eventmachine` -- `sudo gem install rxruby` +- `sudo gem install rx` ### Run the application @@ -36,4 +42,3 @@ Windows 11 Raspberry Pi 5 ![image](https://github.com/user-attachments/assets/190f8603-a6db-45c6-a5f0-cfd4dc1b87e2) - From 392b92d5c43e26adde50cde0d7e1b1fed582f71c Mon Sep 17 00:00:00 2001 From: "andrea.mancuso" Date: Fri, 7 Feb 2025 18:44:13 +0100 Subject: [PATCH 7/9] Got the fecking button to render at least/last --- .idea/.gitignore | 8 ---- .idea/misc.xml | 4 -- .idea/modules.xml | 8 ---- .idea/vcs.xml | 6 --- .idea/xframes-ruby.iml | 15 -------- .ruby-version | Bin 16 -> 16 bytes README.md | 4 +- main.bat | 4 ++ main.ps1 | 3 ++ main.rb | 84 ++++++++++++++++++++++------------------- sampleapp.rb | 58 ++++++++++++++++++++-------- services.rb | 4 +- treetraversal.rb | 38 ++++++++++++++++--- widgetnode.rb | 10 ++--- 14 files changed, 136 insertions(+), 110 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml delete mode 100644 .idea/xframes-ruby.iml create mode 100644 main.bat create mode 100644 main.ps1 diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index d959ea1..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8d02977..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/xframes-ruby.iml b/.idea/xframes-ruby.iml deleted file mode 100644 index 6e7c09c..0000000 --- a/.idea/xframes-ruby.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.ruby-version b/.ruby-version index 0e8a452451658bf9ae97aa3cc7d6ec24d96e5e5d..eaf79aea3d1ce334ba3b4e54b6d34252377d6e1b 100644 GIT binary patch literal 16 VcmezW&zM1v!3c;=7 latest_app_state.todo_text, - "todo_items" => latest_app_state.todo_items - }) + @sample_app_state = Rx::BehaviorSubject.new(AppState.new("", [])) + + @props.on_next({ + "todo_text" => "", + "todo_items" => [] + }) + + puts "App initialize called 3" + + promise = Concurrent::Promise.execute do + @sample_app_state.subscribe do |latest_app_state| + # Safe operation, as Async ensures execution is handled in the appropriate thread + @props.on_next({ + "todo_text" => latest_app_state.todo_text, + "todo_items" => latest_app_state.todo_items + }) + end end + + # Wait for the promise to complete before moving forward + promise.wait + + puts "App initialize called 4" end def render - children = [button("Add todo", method(:on_click), button_style)] - - 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 + puts "App render called" + children = [button("Add todo", Proc.new {puts "suga"}, $button_style)] + # children = [button("Add todo")] + + puts "App render called 2" + + # props.value["todo_items"].each do |todo_item| + # text = "#{todo_item.text} (#{todo_item.done ? 'done' : 'to do'})." + # children << unformatted_text(text, $text_style) + # children << unformatted_text(text) + # end + + puts "App render called 3" node(children) end diff --git a/services.rb b/services.rb index 713e01a..8d8724c 100644 --- a/services.rb +++ b/services.rb @@ -10,7 +10,7 @@ def initialize @id_event_registration_lock = Mutex.new @events_subject = Rx::ReplaySubject.new(10) - @events_subject.debounce(0.001).subscribe { |fn| fn.call } + # @events_subject.debounce(0.001).subscribe { |fn| fn.call } @widget_registry = {} @on_click_registry = Rx::BehaviorSubject.new({}) @@ -65,7 +65,7 @@ def dispatch_on_click_event(widget_id) end def create_widget(widget) - widget_json = widget.to_serializable_dict.to_json + widget_json = widget.to_serializable_hash.to_json set_element(widget_json) end diff --git a/treetraversal.rb b/treetraversal.rb index d3e0b21..9bcb4a4 100644 --- a/treetraversal.rb +++ b/treetraversal.rb @@ -16,11 +16,11 @@ def initialize(id, renderable) @children_change_subscription = nil end - def to_dict + def to_hash { "id" => @id, "current_props" => @current_props, - "children" => @children.map(&:to_dict) + "children" => @children.map(&:to_hash) } end @@ -53,6 +53,8 @@ def subscribe_to_props_helper(shadow_node) shadow_node.props_change_subscription.dispose end + return nil + if shadow_node.renderable.is_a?(BaseComponent) component = shadow_node.renderable shadow_node.props_change_subscription = component.props.pipe( @@ -66,7 +68,7 @@ def subscribe_to_props_helper(shadow_node) end def handle_widget_node(widget) - if widget.type == WidgetTypes::Button + if widget.type == WidgetTypes[:Button] on_click = widget.props["on_click"] if on_click @widget_registration_service.register_on_click(widget.id, on_click) @@ -100,30 +102,54 @@ def handle_widget_node_props_change(shadow_node, widget_node, new_props) end def traverse_tree(renderable) + puts "a" + if renderable.is_a?(BaseComponent) - shadow_child = traverse_tree(renderable.render) + puts "b" + rendered_child = renderable.render + shadow_child = traverse_tree(rendered_child) + puts "c" id = @widget_registration_service.get_next_component_id + puts "d" shadow_node = ShadowNode.new(id, renderable) + puts "e" shadow_node.children = [shadow_child] - shadow_node.current_props = renderable.props.value + puts "f" + # shadow_node.current_props = renderable.props.value + puts "g" subscribe_to_props_helper(shadow_node) + puts "h" return shadow_node elsif renderable.is_a?(WidgetNode) + puts "i" id = @widget_registration_service.get_next_widget_id - raw_node = WidgetNode.create_raw_childless_widget_node_with_id(id, renderable) + puts "j" + raw_node = create_raw_childless_widget_node_with_id(id, renderable) + puts "k" handle_widget_node(raw_node) + puts "l" @widget_registration_service.create_widget(raw_node) + puts "m" shadow_node = ShadowNode.new(id, renderable) + puts "n" shadow_node.children = renderable.children.value.map { |child| traverse_tree(child) } + puts "o" shadow_node.current_props = renderable.props.value + puts "p" linkable_children = shadow_node.get_linkable_children + puts "q" if !linkable_children.empty? + puts "r" @widget_registration_service.link_children(id, linkable_children.map(&:id)) end + puts "s" + subscribe_to_props_helper(shadow_node) + + puts "t" return shadow_node else raise 'Unrecognised renderable' diff --git a/widgetnode.rb b/widgetnode.rb index 6007c57..d2f69c5 100644 --- a/widgetnode.rb +++ b/widgetnode.rb @@ -40,7 +40,7 @@ def to_serializable_hash } @props.each do |key, value| - out[key] = value unless value.is_a?(Proc) + out[key] = value unless value.is_a?(Proc) || value.is_a?(WidgetStyleDef) || value.is_a?(NodeStyleDef) end out @@ -72,21 +72,21 @@ def root_node(children, style = nil) props = init_props_with_style(style) props['root'] = true - widget_node_factory(WidgetTypes::Node, props, children) + 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) + 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, []) + widget_node_factory(WidgetTypes[:UnformattedText], props, []) end def button(label, on_click = nil, style = nil) @@ -96,6 +96,6 @@ def button(label, on_click = nil, style = nil) props['label'] = label props['on_click'] = on_click if on_click - widget_node_factory(WidgetTypes::Button, props, []) + widget_node_factory(WidgetTypes[:Button], props, []) end From fbad9e54c02a76a1fab903af623d640a93ac289b Mon Sep 17 00:00:00 2001 From: Andrea Mancuso Date: Fri, 7 Feb 2025 18:28:19 +0100 Subject: [PATCH 8/9] Looks like I fixed the style --- .idea/workspace.xml | 88 +++++++++++++++++++++++++++++++++++++++++++++ services.rb | 2 +- treetraversal.rb | 22 ------------ widgetnode.rb | 10 ++++-- 4 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 .idea/workspace.xml diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000..39f0bd1 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1738875470426 + + + + + + + + + \ No newline at end of file diff --git a/services.rb b/services.rb index 8d8724c..7f113bb 100644 --- a/services.rb +++ b/services.rb @@ -65,7 +65,7 @@ def dispatch_on_click_event(widget_id) end def create_widget(widget) - widget_json = widget.to_serializable_hash.to_json + widget_json = widget.to_hash.to_json set_element(widget_json) end diff --git a/treetraversal.rb b/treetraversal.rb index 9bcb4a4..6e0e5bb 100644 --- a/treetraversal.rb +++ b/treetraversal.rb @@ -102,54 +102,32 @@ def handle_widget_node_props_change(shadow_node, widget_node, new_props) end def traverse_tree(renderable) - puts "a" - if renderable.is_a?(BaseComponent) - puts "b" rendered_child = renderable.render shadow_child = traverse_tree(rendered_child) - puts "c" id = @widget_registration_service.get_next_component_id - puts "d" shadow_node = ShadowNode.new(id, renderable) - puts "e" shadow_node.children = [shadow_child] - puts "f" # shadow_node.current_props = renderable.props.value - puts "g" subscribe_to_props_helper(shadow_node) - puts "h" return shadow_node elsif renderable.is_a?(WidgetNode) - puts "i" id = @widget_registration_service.get_next_widget_id - puts "j" raw_node = create_raw_childless_widget_node_with_id(id, renderable) - puts "k" handle_widget_node(raw_node) - puts "l" @widget_registration_service.create_widget(raw_node) - puts "m" shadow_node = ShadowNode.new(id, renderable) - puts "n" shadow_node.children = renderable.children.value.map { |child| traverse_tree(child) } - puts "o" shadow_node.current_props = renderable.props.value - puts "p" linkable_children = shadow_node.get_linkable_children - puts "q" if !linkable_children.empty? - puts "r" @widget_registration_service.link_children(id, linkable_children.map(&:id)) end - puts "s" - subscribe_to_props_helper(shadow_node) - puts "t" return shadow_node else raise 'Unrecognised renderable' diff --git a/widgetnode.rb b/widgetnode.rb index d2f69c5..de86288 100644 --- a/widgetnode.rb +++ b/widgetnode.rb @@ -33,14 +33,20 @@ def initialize(id, type, props = {}) @props = props end - def to_serializable_hash + def to_hash out = { 'id' => @id, 'type' => @type.to_s } @props.each do |key, value| - out[key] = value unless value.is_a?(Proc) || value.is_a?(WidgetStyleDef) || value.is_a?(NodeStyleDef) + 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 From 67fda5338beea280e65ac0942c314e13e22967fe Mon Sep 17 00:00:00 2001 From: Andrea Mancuso Date: Fri, 7 Feb 2025 19:25:24 +0100 Subject: [PATCH 9/9] Not quite working --- main.rb | 53 ++++------------------------------------- sampleapp.rb | 62 ++++++++++++++++++++++-------------------------- services.rb | 36 +++++++++++++++++++++------- treetraversal.rb | 26 +++++++++++--------- 4 files changed, 75 insertions(+), 102 deletions(-) diff --git a/main.rb b/main.rb index c96c607..3c86be8 100644 --- a/main.rb +++ b/main.rb @@ -94,60 +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 - service = WidgetRegistrationService.new shadow_node_traversal_helper = ShadowNodeTraversalHelper.new(service) on_init = FFI::Function.new(:void, []) do - # Create an Async task using concurrent-ruby's Async - # Concurrent::Async.perform do - # Thread.main do - puts "OnInit called!" + root = Root.new() - root = Root.new() - - puts "After root creation" - - shadow_node_traversal_helper.traverse_tree(root) - - puts "After tree" - # end - # end + shadow_node_traversal_helper.traverse_tree(root) end on_text_changed = FFI::Function.new(:void, [:int, :string]) do |id, text| @@ -172,6 +125,8 @@ def to_json(*options) end on_click = FFI::Function.new(:void, [:int]) do |id| + service.dispatch_on_click_event(id) + puts "Button clicked: ID=#{id}" end diff --git a/sampleapp.rb b/sampleapp.rb index 361e564..62d9ca3 100644 --- a/sampleapp.rb +++ b/sampleapp.rb @@ -25,16 +25,17 @@ def initialize(todo_text, todo_items) $sample_app_state = Rx::BehaviorSubject.new(AppState.new("", [])) def on_click - 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] - ) + 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 - $sample_app_state.on_next(new_state) + promise.wait end $text_style = WidgetStyle.new( @@ -62,18 +63,10 @@ class App < BaseComponent def initialize super({}) - @sample_app_state = Rx::BehaviorSubject.new(AppState.new("", [])) - - @props.on_next({ - "todo_text" => "", - "todo_items" => [] - }) - - puts "App initialize called 3" - promise = Concurrent::Promise.execute do - @sample_app_state.subscribe do |latest_app_state| - # Safe operation, as Async ensures execution is handled in the appropriate thread + $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 @@ -81,26 +74,27 @@ def initialize end end - # Wait for the promise to complete before moving forward promise.wait - puts "App initialize called 4" + @props.on_next({ + "todo_text" => "", + "todo_items" => [TodoItem.new("New Todo", false)] + }) end def render - puts "App render called" - children = [button("Add todo", Proc.new {puts "suga"}, $button_style)] - # children = [button("Add todo")] - - puts "App render called 2" - - # props.value["todo_items"].each do |todo_item| - # text = "#{todo_item.text} (#{todo_item.done ? 'done' : 'to do'})." - # children << unformatted_text(text, $text_style) - # children << unformatted_text(text) - # end + children = [button("Add todo", Proc.new { + on_click() + }, $button_style)] - puts "App render called 3" + 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 diff --git a/services.rb b/services.rb index 7f113bb..30a65b4 100644 --- a/services.rb +++ b/services.rb @@ -3,15 +3,25 @@ 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(10) - # @events_subject.debounce(0.001).subscribe { |fn| fn.call } - + + # @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({}) @@ -56,12 +66,22 @@ def register_on_click(widget_id, on_click) end def dispatch_on_click_event(widget_id) - on_click = @on_click_registry.value[widget_id] - if on_click - @events_subject.on_next(on_click) - else - puts "Widget with id #{widget_id} has no on_click handler" + 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) diff --git a/treetraversal.rb b/treetraversal.rb index 6e0e5bb..8284271 100644 --- a/treetraversal.rb +++ b/treetraversal.rb @@ -53,18 +53,22 @@ def subscribe_to_props_helper(shadow_node) shadow_node.props_change_subscription.dispose end - return nil - - 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) } + 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)