From ba2234fc600ca50469702806476e38cbe3cb670a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Helmut=20H=C3=A4nsel?= <helmut.haensel@gmx.de>
Date: Mon, 13 Jan 2025 08:03:10 +0100
Subject: [PATCH] support both structs and instances of structs as mixins, fix
 type determination in `@vars`

---
 src/ReactiveTools.jl      |  48 +++--------------
 src/stipple/reactivity.jl | 108 ++++++++++++++++++++++++++++++++------
 2 files changed, 98 insertions(+), 58 deletions(-)

diff --git a/src/ReactiveTools.jl b/src/ReactiveTools.jl
index 11d4ad3..83dad1e 100644
--- a/src/ReactiveTools.jl
+++ b/src/ReactiveTools.jl
@@ -5,7 +5,7 @@ using MacroTools
 using MacroTools: postwalk
 using OrderedCollections
 import Genie
-import Stipple: deletemode!, parse_expression!, parse_expression, init_storage, striplines, striplines!, postwalk!
+import Stipple: deletemode!, parse_expression!, parse_expression, init_storage, striplines, striplines!, postwalk!, add_brackets!, required_evals!, parse_mixin_params
 
 #definition of handlers/events
 export @onchange, @onbutton, @event, @notify
@@ -538,22 +538,6 @@ macro define_var()
   end |> esc
 end
 
-function parse_mixin_params(params)
-  striplines!(params)
-  mixin, prefix, postfix = if length(params) == 1 && params[1] isa Expr && hasproperty(params[1], :head) && params[1].head == :(::)
-    params[1].args[2], string(params[1].args[1]), ""
-  elseif length(params) == 1
-    params[1], "", ""
-  elseif length(params) == 2
-    params[1], string(params[2]), ""
-  elseif length(params) == 3
-    params[1], string(params[2]), string(params[3])
-  else
-    error("1, 2, or 3 arguments expected, found $(length(params))")
-  end
-  mixin, prefix, postfix
-end
-
 function parse_macros(expr::Expr, storage::LittleDict, m::Module, let_block::Expr = nothing, vars::Set = Set())
   expr.head == :macrocall || return expr
   flag = :nothing
@@ -562,7 +546,7 @@ function parse_macros(expr::Expr, storage::LittleDict, m::Module, let_block::Exp
 
   source = filter(x -> x isa LineNumberNode, expr.args)
   source = isempty(source) ? "" : last(source)
-  striplines!(expr)
+  expr = striplines(expr)
   params = expr.args[2:end]
 
   if fn != :mixin
@@ -579,7 +563,7 @@ function parse_macros(expr::Expr, storage::LittleDict, m::Module, let_block::Exp
     storage[var] = ex
   elseif fn == :mixin
     mixin, prefix, postfix = parse_mixin_params(params)
-    mixin_storage = Stipple.model_to_storage(@eval(m, $mixin), prefix, postfix)
+    mixin_storage = Stipple.var_to_storage(@eval(m, $mixin), prefix, postfix; mixin_name = mixin)
     pre_length = lastindex(prefix)
     post_length = lastindex(postfix)
 
@@ -713,7 +697,9 @@ function get_varnames(app_expr::Vector, context::Module)
           push!(varnames, res isa Symbol ? res : res[1])
       elseif ex.args[1] == Symbol("@mixin")
           mixin, prefix, postfix = parse_mixin_params(ex.args[2:end])
-          fnames = setdiff(@eval(context, collect($mixin isa LittleDict ? keys($mixin) : propertynames($mixin()))), Stipple.AUTOFIELDS, Stipple.INTERNALFIELDS)
+          mixin_val = @eval(context, $mixin)
+          mixin_val isa DataType && mixin_val <: ReactiveModel && (mixin_val = Stipple.get_concrete_type(mixin_val))
+          fnames = setdiff(collect(mixin_val isa LittleDict ? keys(mixin_val) : mixin_val isa DataType ? fieldnames(mixin_val) : propertynames(mixin_val)), Stipple.AUTOFIELDS, Stipple.INTERNALFIELDS)
           prefix === nothing || (fnames = Symbol.(prefix, fnames, postfix))
           append!(varnames, fnames)
       end
@@ -725,26 +711,6 @@ function get_varnames(app_expr::Vector, context::Module)
   varnames
 end
 
-function add_brackets!(expr, varnames)
-  expr isa Expr || return expr
-  ex = Stipple.find_assignment(expr)
-  ex === nothing && return expr
-  val = ex.args[end]
-  if val isa Symbol && val ∈ varnames
-    ex.args[end] = :($val[])
-    return expr
-  elseif val isa Expr
-    postwalk!(val) do x
-      if x isa Symbol && x ∈ varnames
-        :($x[])
-      else
-        x
-      end
-    end
-  end
-  expr
-end
-
 macro app(typename, expr, handlers_fn_name = Symbol(typename, :_handlers), mixin = false)
   :(Stipple.ReactiveTools.@handlers $typename $expr $handlers_fn_name $mixin) |> esc
 end
@@ -801,7 +767,7 @@ macro handlers(typename, expr, handlers_fn_name = Symbol(typename, :_handlers),
   filter!(x -> !isa(x, LineNumberNode), initcode)
   let_block = Expr(:block, :(_ = 0))
   required_vars = Set()
-  Stipple.required_evals!.(initcode, Ref(required_vars))
+  required_evals!.(initcode, Ref(required_vars))
   parse_macros.(initcode, Ref(storage), Ref(__module__), Ref(let_block), Ref(required_vars))
   # if no initcode is provided and typename is already defined, don't overwrite the existing type and just declare the handlers function
   initcode_final = isempty(initcode) && isdefined(__module__, typename) ? Expr(:block) : :(Stipple.@type($typename, $storage))
diff --git a/src/stipple/reactivity.jl b/src/stipple/reactivity.jl
index b7992df..6332e92 100644
--- a/src/stipple/reactivity.jl
+++ b/src/stipple/reactivity.jl
@@ -184,14 +184,46 @@ function split_expr(expr)
   expr.args[1] isa Symbol ? (expr.args[1], nothing, expr.args[2]) : (expr.args[1].args[1], expr.args[1].args[2], expr.args[2])
 end
 
-function model_to_storage(::Type{T}, prefix = "", postfix = "") where T# <: ReactiveModel
-  M = T <: ReactiveModel ? get_concrete_type(T) : T
-  fields = fieldnames(M)
-  values = getfield.(Ref(M()), fields)
+function expr_isa_var(ex)
+  ex isa Symbol && return true
+  while ex isa Expr
+    if ex.head == :call && ex.args[1] in (:getfield, :getproperty) || ex.head == :.
+      ex = ex.args[2]
+    else
+      return false
+    end
+  end
+
+  return ex isa QuoteNode && ex.value isa Symbol
+end
+
+function var_to_storage(T, prefix = "", postfix = ""; mode = READONLY, mixin_name = nothing)
+  M, m = if T isa DataType
+    T <: ReactiveModel && (T = get_concrete_type(T))
+    T, T()
+  else
+    typeof(T), T
+  end
+
+  fields = collect(fieldnames(M))
+  values = Any[getfield.(Ref(m), fields)...]
+  ftypes = Any[fieldtypes(M)...]
+  has_reactives = any(ftypes .<: Reactive)
+
+  # if m has no reactive fields, we assume that all fields should be made reactive, default mode is READONLY
+  if !has_reactives
+    for (i, (f, type, v)) in enumerate(zip(fields, values, ftypes, values))
+      f in [INTERNALFIELDS..., AUTOFIELDS...] && continue
+      rtype = Reactive{type}
+      ftypes[i] = rtype
+      values[i] = expr_isa_var(mixin_name) ? Expr(:call, rtype, Expr(:., mixin_name, QuoteNode(f)), mode) : rtype(v, mode)
+    end
+  end
   storage = LittleDict{Symbol, Expr}()
-  for (f, type, v) in zip(fields, fieldtypes(M), values)
+  for (f, type, v) in zip(fields, ftypes, values)
     f = f in [INTERNALFIELDS..., AUTOFIELDS...] ? f : Symbol(prefix, f, postfix)
-    storage[f] = v isa Symbol ? :($f::$type = $(QuoteNode(v))) : :($f::$type = Stipple._deepcopy($v))
+    v isa Symbol && (v = QuoteNode(v))
+    storage[f] = v isa QuoteNode || v isa Expr ? :($f::$type = $v) : :($f::$type = Stipple._deepcopy($v))
   end
   # fix channel field, which is not reconstructed properly by the code above
   storage[:channel__] = :(channel__::String = Stipple.channelfactory())
@@ -374,6 +406,42 @@ end
 
 parse_expression(expr::Expr, mode = nothing, source = nothing, m = nothing, let_block::Union{Expr, Nothing} = nothing, vars::Set = Set()) = parse_expression!(copy(expr), mode, source, m, let_block, vars)
 
+function parse_mixin_params(params)
+  striplines!(params)
+  mixin, prefix, postfix = if length(params) == 1 && params[1] isa Expr && hasproperty(params[1], :head) && params[1].head == :(::)
+    params[1].args[2], string(params[1].args[1]), ""
+  elseif length(params) == 1
+    params[1], "", ""
+  elseif length(params) == 2
+    params[1], string(params[2]), ""
+  elseif length(params) == 3
+    params[1], string(params[2]), string(params[3])
+  else
+    error("1, 2, or 3 arguments expected, found $(length(params))")
+  end
+  mixin, prefix, postfix
+end
+
+function add_brackets!(expr, varnames)
+  expr isa Expr || return expr
+  ex = Stipple.find_assignment(expr)
+  ex === nothing && return expr
+  val = ex.args[end]
+  if val isa Symbol && val ∈ varnames
+    ex.args[end] = :($val[])
+    return expr
+  elseif val isa Expr
+    postwalk!(val) do x
+      if x isa Symbol && x ∈ varnames
+        :($x[])
+      else
+        x
+      end
+    end
+  end
+  expr
+end
+
 macro var_storage(expr)
   m = __module__
   if expr.head != :block
@@ -386,6 +454,7 @@ macro var_storage(expr)
   required_vars = Set()
   let_block = Expr(:block, :(_ = 0))
   required_evals!.(expr.args, Ref(required_vars))
+  add_brackets!.(expr.args, Ref(required_vars))
   for e in expr.args
       if e isa LineNumberNode
           source = e
@@ -415,17 +484,22 @@ macro var_storage(expr)
       else
         # parse @mixin call, which is now only defined in ReactiveTools, but wouldn't work here
         if e.head == :macrocall && (e.args[1] == Symbol("@mixin") || e.args[1] == Symbol("@mix_in"))
-          e.args = filter!(x -> ! isa(x, LineNumberNode), e.args)
-          prefix = postfix = ""
-          if e.args[2] isa Expr && e.args[2].head == :(::)
-            prefix = string(e.args[2].args[1])
-            e.args[2] = e.args[2].args[2]
-          else
-            length(e.args) ≥ 3 && (prefix = string(e.args[3]))
-            length(e.args) ≥ 4 && (postfix = string(e.args[4]))
-          end
-
-          mixin_storage = @eval __module__ Stipple.model_to_storage($(e.args[2]), $prefix, $postfix)
+          # e.args = filter!(x -> ! isa(x, LineNumberNode), e.args)
+          # prefix = postfix = ""
+          # if e.args[2] isa Expr && e.args[2].head == :(::)
+          #   prefix = string(e.args[2].args[1])
+          #   e.args[2] = e.args[2].args[2]
+          # else
+          #   length(e.args) ≥ 3 && (prefix = string(e.args[3]))
+          #   length(e.args) ≥ 4 && (postfix = string(e.args[4]))
+          # end
+
+          # mixin = e.args[2]
+          params = e.args[2:end]
+          mixin, prefix, postfix = parse_mixin_params(params)
+          mixin_storage = Stipple.var_to_storage(@eval(__module__, $mixin), prefix, postfix; mixin_name = mixin)
+      
+#          mixin_storage = Stipple.var_to_storage(@eval(__module__, $mixin), prefix, postfix; mixin_name = mixin)
           storage = merge_storage(storage, mixin_storage; context = __module__)
         end
         :modes__, e