Explorar el Código

app/schema: add wallet schema

epiphany hace 4 meses
padre
commit
c11d2c8059

+ 1183 - 0
bin/app/data/copy.obj

@@ -0,0 +1,1183 @@
+# Blender 4.5.6 LTS
+# www.blender.org
+mtllib copy.mtl
+o Curve
+v 0.324668 0.000000 -0.846560
+v 0.321363 0.000000 -0.848484
+v 0.318075 0.000000 -0.850409
+v 0.314801 0.000000 -0.852337
+v 0.311541 0.000000 -0.854270
+v 0.308293 0.000000 -0.856209
+v 0.305055 0.000000 -0.858155
+v 0.301826 0.000000 -0.860110
+v 0.298605 0.000000 -0.862075
+v 0.295390 0.000000 -0.864052
+v 0.292179 0.000000 -0.866043
+v 0.288971 0.000000 -0.868048
+v 0.285764 0.000000 -0.870070
+v 0.285167 0.000000 -0.870440
+v 0.283516 0.000000 -0.871463
+v 0.281022 0.000000 -0.873008
+v 0.277895 0.000000 -0.874946
+v 0.274347 0.000000 -0.877144
+v 0.270588 0.000000 -0.879474
+v 0.266829 0.000000 -0.881803
+v 0.263281 0.000000 -0.884001
+v 0.260155 0.000000 -0.885939
+v 0.257661 0.000000 -0.887484
+v 0.256010 0.000000 -0.888507
+v 0.255412 0.000000 -0.888877
+v 0.255412 0.000000 -0.908113
+v -0.213599 0.000000 -0.908113
+v -0.682181 0.000000 -0.908113
+v -0.682181 0.000000 -0.889305
+v -0.712536 0.000000 -0.870497
+v -0.715751 0.000000 -0.868475
+v -0.718980 0.000000 -0.866470
+v -0.722222 0.000000 -0.864480
+v -0.725473 0.000000 -0.862502
+v -0.728729 0.000000 -0.860537
+v -0.731989 0.000000 -0.858582
+v -0.735248 0.000000 -0.856636
+v -0.738505 0.000000 -0.854698
+v -0.741755 0.000000 -0.852765
+v -0.744997 0.000000 -0.850837
+v -0.748227 0.000000 -0.848911
+v -0.751442 0.000000 -0.846988
+v -0.751416 0.000000 -0.821319
+v -0.751347 0.000000 -0.750353
+v -0.751241 0.000000 -0.643149
+v -0.751109 0.000000 -0.508766
+v -0.750959 0.000000 -0.356264
+v -0.750800 0.000000 -0.194703
+v -0.750642 0.000000 -0.033141
+v -0.750492 0.000000 0.119361
+v -0.750360 0.000000 0.253744
+v -0.750254 0.000000 0.360948
+v -0.750184 0.000000 0.431914
+v -0.750159 0.000000 0.457583
+v -0.747265 0.000000 0.459300
+v -0.744356 0.000000 0.461034
+v -0.741435 0.000000 0.462783
+v -0.738505 0.000000 0.464546
+v -0.735569 0.000000 0.466320
+v -0.732630 0.000000 0.468105
+v -0.729691 0.000000 0.469900
+v -0.726755 0.000000 0.471701
+v -0.723825 0.000000 0.473509
+v -0.720904 0.000000 0.475322
+v -0.717995 0.000000 0.477138
+v -0.715101 0.000000 0.478956
+v -0.714504 0.000000 0.479326
+v -0.712852 0.000000 0.480349
+v -0.710358 0.000000 0.481894
+v -0.707231 0.000000 0.483830
+v -0.703683 0.000000 0.486028
+v -0.699923 0.000000 0.488356
+v -0.696164 0.000000 0.490685
+v -0.692616 0.000000 0.492883
+v -0.689489 0.000000 0.494819
+v -0.686994 0.000000 0.496364
+v -0.685343 0.000000 0.497387
+v -0.684746 0.000000 0.497757
+v -0.684746 0.000000 0.514859
+v -0.664652 0.000000 0.514859
+v -0.665934 0.000000 -0.707212
+v -0.633869 0.000000 -0.725592
+v -0.630769 0.000000 -0.727409
+v -0.627668 0.000000 -0.729226
+v -0.624563 0.000000 -0.731043
+v -0.621454 0.000000 -0.732859
+v -0.618340 0.000000 -0.734676
+v -0.615218 0.000000 -0.736493
+v -0.612086 0.000000 -0.738309
+v -0.608945 0.000000 -0.740126
+v -0.605792 0.000000 -0.741943
+v -0.602625 0.000000 -0.743759
+v -0.599443 0.000000 -0.745576
+v -0.596246 0.000000 -0.747392
+v -0.596246 0.000000 -0.748873
+v -0.596246 0.000000 -0.752965
+v -0.596246 0.000000 -0.759147
+v -0.596246 0.000000 -0.766897
+v -0.596246 0.000000 -0.775691
+v -0.596246 0.000000 -0.785008
+v -0.596246 0.000000 -0.794325
+v -0.596246 0.000000 -0.803119
+v -0.596246 0.000000 -0.810868
+v -0.596246 0.000000 -0.817051
+v -0.596246 0.000000 -0.821143
+v -0.596246 0.000000 -0.822623
+v 0.325097 0.000000 -0.822623
+v 0.325097 0.000000 -0.846560
+v 0.325089 0.000000 -0.846560
+v 0.325065 0.000000 -0.846560
+v 0.325030 0.000000 -0.846560
+v 0.324986 0.000000 -0.846560
+v 0.324936 0.000000 -0.846560
+v 0.324883 0.000000 -0.846560
+v 0.324830 0.000000 -0.846560
+v 0.324780 0.000000 -0.846560
+v 0.324735 0.000000 -0.846560
+v 0.324700 0.000000 -0.846560
+v 0.324677 0.000000 -0.846560
+v 0.471867 0.000000 0.727720
+v 0.471867 0.000000 0.712333
+v 0.501798 0.000000 0.693095
+v 0.504790 0.000000 0.691279
+v 0.507780 0.000000 0.689462
+v 0.510767 0.000000 0.687645
+v 0.513750 0.000000 0.685828
+v 0.516727 0.000000 0.684011
+v 0.519697 0.000000 0.682194
+v 0.522658 0.000000 0.680377
+v 0.525608 0.000000 0.678560
+v 0.528547 0.000000 0.676742
+v 0.531473 0.000000 0.674926
+v 0.534385 0.000000 0.673109
+v 0.537280 0.000000 0.671293
+v 0.537305 0.000000 0.646104
+v 0.537375 0.000000 0.576463
+v 0.537481 0.000000 0.471261
+v 0.537614 0.000000 0.339389
+v 0.537764 0.000000 0.189735
+v 0.537923 0.000000 0.031192
+v 0.538083 0.000000 -0.127352
+v 0.538233 0.000000 -0.277005
+v 0.538366 0.000000 -0.408878
+v 0.538471 0.000000 -0.514079
+v 0.538541 0.000000 -0.583720
+v 0.538567 0.000000 -0.608909
+v 0.539845 0.000000 -0.608909
+v 0.539845 0.000000 -0.632846
+v 0.536540 0.000000 -0.634770
+v 0.533252 0.000000 -0.636695
+v 0.529978 0.000000 -0.638623
+v 0.526718 0.000000 -0.640556
+v 0.523470 0.000000 -0.642494
+v 0.520232 0.000000 -0.644440
+v 0.517003 0.000000 -0.646395
+v 0.513782 0.000000 -0.648361
+v 0.510566 0.000000 -0.650338
+v 0.507355 0.000000 -0.652328
+v 0.504147 0.000000 -0.654334
+v 0.500940 0.000000 -0.656355
+v 0.500343 0.000000 -0.656725
+v 0.498692 0.000000 -0.657749
+v 0.496197 0.000000 -0.659294
+v 0.493069 0.000000 -0.661231
+v 0.489520 0.000000 -0.663430
+v 0.485760 0.000000 -0.665759
+v 0.482001 0.000000 -0.668088
+v 0.478452 0.000000 -0.670287
+v 0.475324 0.000000 -0.672224
+v 0.472829 0.000000 -0.673770
+v 0.471178 0.000000 -0.674793
+v 0.470581 0.000000 -0.675163
+v 0.470581 0.000000 -0.694398
+v 0.001576 0.000000 -0.694398
+v -0.467004 0.000000 -0.694398
+v -0.467004 0.000000 -0.675163
+v -0.497360 0.000000 -0.656355
+v -0.500575 0.000000 -0.654334
+v -0.503805 0.000000 -0.652328
+v -0.507046 0.000000 -0.650338
+v -0.510297 0.000000 -0.648361
+v -0.513553 0.000000 -0.646395
+v -0.516813 0.000000 -0.644440
+v -0.520072 0.000000 -0.642494
+v -0.523329 0.000000 -0.640556
+v -0.526579 0.000000 -0.638623
+v -0.529821 0.000000 -0.636695
+v -0.533051 0.000000 -0.634770
+v -0.536266 0.000000 -0.632846
+v -0.536241 0.000000 -0.607733
+v -0.536171 0.000000 -0.538301
+v -0.536065 0.000000 -0.433415
+v -0.535933 0.000000 -0.301939
+v -0.535783 0.000000 -0.152735
+v -0.535624 0.000000 0.005333
+v -0.535465 0.000000 0.163400
+v -0.535315 0.000000 0.312604
+v -0.535183 0.000000 0.444081
+v -0.535078 0.000000 0.548967
+v -0.535008 0.000000 0.618399
+v -0.534983 0.000000 0.643512
+v -0.536266 0.000000 0.643512
+v -0.536266 0.000000 0.671722
+v -0.533282 0.000000 0.673439
+v -0.530314 0.000000 0.675174
+v -0.527361 0.000000 0.676923
+v -0.524421 0.000000 0.678687
+v -0.521494 0.000000 0.680462
+v -0.518576 0.000000 0.682248
+v -0.515668 0.000000 0.684042
+v -0.512767 0.000000 0.685844
+v -0.509872 0.000000 0.687652
+v -0.506981 0.000000 0.689464
+v -0.504094 0.000000 0.691279
+v -0.501207 0.000000 0.693095
+v -0.500610 0.000000 0.693465
+v -0.498959 0.000000 0.694488
+v -0.496464 0.000000 0.696034
+v -0.493338 0.000000 0.697971
+v -0.489789 0.000000 0.700170
+v -0.486030 0.000000 0.702500
+v -0.482271 0.000000 0.704829
+v -0.478722 0.000000 0.707028
+v -0.475596 0.000000 0.708965
+v -0.473101 0.000000 0.710511
+v -0.471450 0.000000 0.711534
+v -0.470853 0.000000 0.711904
+v -0.470853 0.000000 0.728998
+v -0.469570 0.000000 0.728998
+v -0.449475 0.000000 0.728998
+v 0.472296 0.000000 0.727720
+v -0.451186 0.000000 -0.493071
+v -0.419121 0.000000 -0.511451
+v -0.416021 0.000000 -0.513268
+v -0.412919 0.000000 -0.515084
+v -0.409815 0.000000 -0.516901
+v -0.406706 0.000000 -0.518718
+v -0.403591 0.000000 -0.520534
+v -0.400469 0.000000 -0.522351
+v -0.397338 0.000000 -0.524168
+v -0.394196 0.000000 -0.525984
+v -0.391043 0.000000 -0.527801
+v -0.387876 0.000000 -0.529618
+v -0.384695 0.000000 -0.531434
+v -0.381497 0.000000 -0.533251
+v -0.381497 0.000000 -0.534731
+v -0.381497 0.000000 -0.538823
+v -0.381497 0.000000 -0.545006
+v -0.381497 0.000000 -0.552755
+v -0.381497 0.000000 -0.561550
+v -0.381497 0.000000 -0.570866
+v -0.381497 0.000000 -0.580183
+v -0.381497 0.000000 -0.588978
+v -0.381497 0.000000 -0.596727
+v -0.381497 0.000000 -0.602909
+v -0.381497 0.000000 -0.607002
+v -0.381497 0.000000 -0.608482
+v 0.453054 0.000000 -0.608482
+v 0.451776 0.000000 0.531944
+v 0.419709 0.000000 0.550325
+v 0.416929 0.000000 0.551929
+v 0.414150 0.000000 0.553534
+v 0.411371 0.000000 0.555142
+v 0.408591 0.000000 0.556754
+v 0.405812 0.000000 0.558372
+v 0.403033 0.000000 0.559997
+v 0.400254 0.000000 0.561631
+v 0.397476 0.000000 0.563276
+v 0.394697 0.000000 0.564933
+v 0.391919 0.000000 0.566603
+v 0.389141 0.000000 0.568289
+v 0.386364 0.000000 0.569991
+v 0.386364 0.000000 0.571446
+v 0.386364 0.000000 0.575469
+v 0.386364 0.000000 0.581546
+v 0.386364 0.000000 0.589163
+v 0.386364 0.000000 0.597808
+v 0.386364 0.000000 0.606966
+v 0.386364 0.000000 0.616124
+v 0.386364 0.000000 0.624769
+v 0.386364 0.000000 0.632386
+v 0.386364 0.000000 0.638463
+v 0.386364 0.000000 0.642486
+v 0.386364 0.000000 0.643941
+v -0.450331 0.000000 0.643941
+v -0.451613 0.000000 -0.492644
+v 0.714603 0.000000 -0.442621
+v 0.684252 0.000000 -0.461428
+v 0.684252 0.000000 -0.480664
+v 0.668004 0.000000 -0.480664
+v 0.666717 0.000000 0.745251
+v 0.634659 0.000000 0.763631
+v 0.631879 0.000000 0.765235
+v 0.629100 0.000000 0.766841
+v 0.626320 0.000000 0.768449
+v 0.623541 0.000000 0.770062
+v 0.620761 0.000000 0.771681
+v 0.617982 0.000000 0.773307
+v 0.615202 0.000000 0.774942
+v 0.612423 0.000000 0.776587
+v 0.609643 0.000000 0.778243
+v 0.606864 0.000000 0.779913
+v 0.604084 0.000000 0.781597
+v 0.601305 0.000000 0.783298
+v 0.601305 0.000000 0.784753
+v 0.601305 0.000000 0.788776
+v 0.601305 0.000000 0.794852
+v 0.601305 0.000000 0.802470
+v 0.601305 0.000000 0.811115
+v 0.601305 0.000000 0.820273
+v 0.601305 0.000000 0.829431
+v 0.601305 0.000000 0.838075
+v 0.601305 0.000000 0.845693
+v 0.601305 0.000000 0.851770
+v 0.601305 0.000000 0.855792
+v 0.601305 0.000000 0.857248
+v -0.321746 0.000000 0.857248
+v -0.321746 0.000000 0.885457
+v -0.318762 0.000000 0.887175
+v -0.315794 0.000000 0.888909
+v -0.312842 0.000000 0.890659
+v -0.309902 0.000000 0.892422
+v -0.306974 0.000000 0.894197
+v -0.304057 0.000000 0.895983
+v -0.301149 0.000000 0.897778
+v -0.298248 0.000000 0.899580
+v -0.295353 0.000000 0.901387
+v -0.292462 0.000000 0.903199
+v -0.289575 0.000000 0.905014
+v -0.286689 0.000000 0.906830
+v -0.286091 0.000000 0.907201
+v -0.284440 0.000000 0.908224
+v -0.281946 0.000000 0.909769
+v -0.278819 0.000000 0.911707
+v -0.275270 0.000000 0.913906
+v -0.271511 0.000000 0.916235
+v -0.267752 0.000000 0.918564
+v -0.264203 0.000000 0.920763
+v -0.261077 0.000000 0.922701
+v -0.258582 0.000000 0.924246
+v -0.256931 0.000000 0.925269
+v -0.256334 0.000000 0.925640
+v -0.256334 0.000000 0.942733
+v 0.686388 0.000000 0.941455
+v 0.686388 0.000000 0.926068
+v 0.716311 0.000000 0.906830
+v 0.719304 0.000000 0.905014
+v 0.722296 0.000000 0.903197
+v 0.725284 0.000000 0.901381
+v 0.728268 0.000000 0.899564
+v 0.731246 0.000000 0.897746
+v 0.734216 0.000000 0.895929
+v 0.737178 0.000000 0.894112
+v 0.740129 0.000000 0.892295
+v 0.743068 0.000000 0.890478
+v 0.745994 0.000000 0.888661
+v 0.748905 0.000000 0.886844
+v 0.751800 0.000000 0.885028
+v 0.751826 0.000000 0.859360
+v 0.751895 0.000000 0.788393
+v 0.752000 0.000000 0.681190
+v 0.752132 0.000000 0.546807
+v 0.752281 0.000000 0.394306
+v 0.752440 0.000000 0.232745
+v 0.752598 0.000000 0.071184
+v 0.752747 0.000000 -0.081318
+v 0.752879 0.000000 -0.215700
+v 0.752984 0.000000 -0.322904
+v 0.753054 0.000000 -0.393871
+v 0.753079 0.000000 -0.419539
+v 0.749774 0.000000 -0.421463
+v 0.746485 0.000000 -0.423388
+v 0.743212 0.000000 -0.425316
+v 0.739952 0.000000 -0.427248
+v 0.736704 0.000000 -0.429187
+v 0.733466 0.000000 -0.431133
+v 0.730237 0.000000 -0.433088
+v 0.727015 0.000000 -0.435053
+v 0.723800 0.000000 -0.437030
+v 0.720589 0.000000 -0.439021
+v 0.717381 0.000000 -0.441026
+v 0.714175 0.000000 -0.443048
+v 0.714183 0.000000 -0.443040
+v 0.714206 0.000000 -0.443016
+v 0.714242 0.000000 -0.442981
+v 0.714286 0.000000 -0.442937
+v 0.714336 0.000000 -0.442887
+v 0.714389 0.000000 -0.442834
+v 0.714442 0.000000 -0.442782
+v 0.714492 0.000000 -0.442732
+v 0.714536 0.000000 -0.442688
+v 0.714572 0.000000 -0.442652
+v 0.714595 0.000000 -0.442629
+vn -0.0000 1.0000 -0.0000
+vn -0.0000 -1.0000 -0.0000
+vt 0.071429 0.000000
+vt 0.066327 0.000000
+vt 0.068878 0.000000
+vt 0.063776 0.000000
+vt 0.061224 0.000000
+vt 0.073980 0.000000
+vt 0.058673 0.000000
+vt 0.056122 0.000000
+vt 0.053571 0.000000
+vt 0.051020 0.000000
+vt 0.048469 0.000000
+vt 0.045918 0.000000
+vt 0.043367 0.000000
+vt 0.040816 0.000000
+vt 0.038265 0.000000
+vt 0.035714 0.000000
+vt 0.033163 0.000000
+vt 0.076531 0.000000
+vt 0.030612 0.000000
+vt 0.028061 0.000000
+vt 0.079082 0.000000
+vt 0.025510 0.000000
+vt 0.081633 0.000000
+vt 0.022959 0.000000
+vt 0.084184 0.000000
+vt 0.020408 0.000000
+vt 0.086735 0.000000
+vt 0.017857 0.000000
+vt 0.089286 0.000000
+vt 0.015306 0.000000
+vt 0.091837 0.000000
+vt 0.012755 0.000000
+vt 0.094388 0.000000
+vt 0.010204 0.000000
+vt 0.096939 0.000000
+vt 0.007653 0.000000
+vt 0.099490 0.000000
+vt 0.005102 0.000000
+vt 0.102041 0.000000
+vt 0.002551 0.000000
+vt 0.104592 0.000000
+vt 0.000000 0.000000
+vt 0.107143 0.000000
+vt 0.301020 0.000000
+vt 0.298469 0.000000
+vt 0.295918 0.000000
+vt 0.293367 0.000000
+vt 0.290816 0.000000
+vt 0.288265 0.000000
+vt 0.285714 0.000000
+vt 0.283163 0.000000
+vt 0.280612 0.000000
+vt 0.278061 0.000000
+vt 0.275510 0.000000
+vt 0.272959 0.000000
+vt 0.267857 0.000000
+vt 0.270408 0.000000
+vt 0.265306 0.000000
+vt 0.109694 0.000000
+vt 0.262755 0.000000
+vt 0.260204 0.000000
+vt 0.257653 0.000000
+vt 0.255102 0.000000
+vt 0.252551 0.000000
+vt 0.250000 0.000000
+vt 0.247449 0.000000
+vt 0.244898 0.000000
+vt 0.242347 0.000000
+vt 0.239796 0.000000
+vt 0.112245 0.000000
+vt 0.204082 0.000000
+vt 0.227041 0.000000
+vt 0.229592 0.000000
+vt 0.232143 0.000000
+vt 0.234694 0.000000
+vt 0.237245 0.000000
+vt 0.224490 0.000000
+vt 0.221939 0.000000
+vt 0.219388 0.000000
+vt 0.216837 0.000000
+vt 0.214286 0.000000
+vt 0.211735 0.000000
+vt 0.209184 0.000000
+vt 0.206633 0.000000
+vt 0.201531 0.000000
+vt 0.446429 0.000000
+vt 0.441327 0.000000
+vt 0.443878 0.000000
+vt 0.438776 0.000000
+vt 0.436224 0.000000
+vt 0.448980 0.000000
+vt 0.433673 0.000000
+vt 0.431122 0.000000
+vt 0.428571 0.000000
+vt 0.426020 0.000000
+vt 0.423469 0.000000
+vt 0.420918 0.000000
+vt 0.418367 0.000000
+vt 0.415816 0.000000
+vt 0.413265 0.000000
+vt 0.410714 0.000000
+vt 0.408163 0.000000
+vt 0.405612 0.000000
+vt 0.451531 0.000000
+vt 0.403061 0.000000
+vt 0.454082 0.000000
+vt 0.400510 0.000000
+vt 0.456633 0.000000
+vt 0.397959 0.000000
+vt 0.459184 0.000000
+vt 0.395408 0.000000
+vt 0.461735 0.000000
+vt 0.392857 0.000000
+vt 0.464286 0.000000
+vt 0.390306 0.000000
+vt 0.466837 0.000000
+vt 0.387755 0.000000
+vt 0.114796 0.000000
+vt 0.469388 0.000000
+vt 0.385204 0.000000
+vt 0.471939 0.000000
+vt 0.382653 0.000000
+vt 0.474490 0.000000
+vt 0.380102 0.000000
+vt 0.477041 0.000000
+vt 0.377551 0.000000
+vt 0.479592 0.000000
+vt 0.375000 0.000000
+vt 0.482143 0.000000
+vt 0.653061 0.000000
+vt 0.369898 0.000000
+vt 0.372449 0.000000
+vt 0.655612 0.000000
+vt 0.367347 0.000000
+vt 0.650510 0.000000
+vt 0.658163 0.000000
+vt 0.484694 0.000000
+vt 0.647959 0.000000
+vt 0.645408 0.000000
+vt 0.642857 0.000000
+vt 0.640306 0.000000
+vt 0.364796 0.000000
+vt 0.637755 0.000000
+vt 0.635204 0.000000
+vt 0.632653 0.000000
+vt 0.630102 0.000000
+vt 0.627551 0.000000
+vt 0.625000 0.000000
+vt 0.487245 0.000000
+vt 0.589286 0.000000
+vt 0.612245 0.000000
+vt 0.614796 0.000000
+vt 0.617347 0.000000
+vt 0.619898 0.000000
+vt 0.622449 0.000000
+vt 0.609694 0.000000
+vt 0.607143 0.000000
+vt 0.604592 0.000000
+vt 0.602041 0.000000
+vt 0.599490 0.000000
+vt 0.596939 0.000000
+vt 0.594388 0.000000
+vt 0.362245 0.000000
+vt 0.591837 0.000000
+vt 0.117347 0.000000
+vt 0.727041 0.000000
+vt 0.724490 0.000000
+vt 0.739796 0.000000
+vt 0.734694 0.000000
+vt 0.737245 0.000000
+vt 0.732143 0.000000
+vt 0.729592 0.000000
+vt 0.974490 0.000000
+vt 0.969388 0.000000
+vt 0.971939 0.000000
+vt 0.977041 0.000000
+vt 0.979592 0.000000
+vt 0.982143 0.000000
+vt 0.984694 0.000000
+vt 0.987245 0.000000
+vt 0.989796 0.000000
+vt 0.992347 0.000000
+vt 0.994898 0.000000
+vt 0.997449 0.000000
+vt 1.000000 0.000000
+vt 0.966837 0.000000
+vt 0.964286 0.000000
+vt 0.961735 0.000000
+vt 0.959184 0.000000
+vt 0.489796 0.000000
+vt 0.956633 0.000000
+vt 0.954082 0.000000
+vt 0.951531 0.000000
+vt 0.948980 0.000000
+vt 0.946429 0.000000
+vt 0.943878 0.000000
+vt 0.941327 0.000000
+vt 0.938776 0.000000
+vt 0.359694 0.000000
+vt 0.936224 0.000000
+vt 0.119898 0.000000
+vt 0.933673 0.000000
+vt 0.492347 0.000000
+vt 0.357143 0.000000
+vt 0.931122 0.000000
+vt 0.122449 0.000000
+vt 0.494898 0.000000
+vt 0.354592 0.000000
+vt 0.928571 0.000000
+vt 0.125000 0.000000
+vt 0.497449 0.000000
+vt 0.352041 0.000000
+vt 0.926020 0.000000
+vt 0.127551 0.000000
+vt 0.500000 0.000000
+vt 0.349490 0.000000
+vt 0.923469 0.000000
+vt 0.130102 0.000000
+vt 0.502551 0.000000
+vt 0.346939 0.000000
+vt 0.132653 0.000000
+vt 0.920918 0.000000
+vt 0.135204 0.000000
+vt 0.137755 0.000000
+vt 0.140306 0.000000
+vt 0.142857 0.000000
+vt 0.145408 0.000000
+vt 0.147959 0.000000
+vt 0.150510 0.000000
+vt 0.153061 0.000000
+vt 0.155612 0.000000
+vt 0.158163 0.000000
+vt 0.196429 0.000000
+vt 0.505102 0.000000
+vt 0.344388 0.000000
+vt 0.160714 0.000000
+vt 0.163265 0.000000
+vt 0.165816 0.000000
+vt 0.168367 0.000000
+vt 0.176020 0.000000
+vt 0.188776 0.000000
+vt 0.191327 0.000000
+vt 0.193878 0.000000
+vt 0.170918 0.000000
+vt 0.173469 0.000000
+vt 0.178571 0.000000
+vt 0.181122 0.000000
+vt 0.183673 0.000000
+vt 0.186224 0.000000
+vt 0.198980 0.000000
+vt 0.660714 0.000000
+vt 0.918367 0.000000
+vt 0.507653 0.000000
+vt 0.663265 0.000000
+vt 0.665816 0.000000
+vt 0.668367 0.000000
+vt 0.670918 0.000000
+vt 0.673469 0.000000
+vt 0.676020 0.000000
+vt 0.678571 0.000000
+vt 0.681122 0.000000
+vt 0.683673 0.000000
+vt 0.686224 0.000000
+vt 0.688776 0.000000
+vt 0.691327 0.000000
+vt 0.693878 0.000000
+vt 0.696429 0.000000
+vt 0.698980 0.000000
+vt 0.341837 0.000000
+vt 0.701531 0.000000
+vt 0.704082 0.000000
+vt 0.706633 0.000000
+vt 0.709184 0.000000
+vt 0.711735 0.000000
+vt 0.510204 0.000000
+vt 0.714286 0.000000
+vt 0.716837 0.000000
+vt 0.719388 0.000000
+vt 0.721939 0.000000
+vt 0.515306 0.000000
+vt 0.512755 0.000000
+vt 0.339286 0.000000
+vt 0.336735 0.000000
+vt 0.517857 0.000000
+vt 0.334184 0.000000
+vt 0.520408 0.000000
+vt 0.331633 0.000000
+vt 0.522959 0.000000
+vt 0.329082 0.000000
+vt 0.525510 0.000000
+vt 0.326531 0.000000
+vt 0.528061 0.000000
+vt 0.323980 0.000000
+vt 0.530612 0.000000
+vt 0.915816 0.000000
+vt 0.321429 0.000000
+vt 0.533163 0.000000
+vt 0.318878 0.000000
+vt 0.535714 0.000000
+vt 0.316327 0.000000
+vt 0.538265 0.000000
+vt 0.313776 0.000000
+vt 0.540816 0.000000
+vt 0.311224 0.000000
+vt 0.543367 0.000000
+vt 0.308673 0.000000
+vt 0.545918 0.000000
+vt 0.548469 0.000000
+vt 0.306122 0.000000
+vt 0.551020 0.000000
+vt 0.553571 0.000000
+vt 0.556122 0.000000
+vt 0.558673 0.000000
+vt 0.561224 0.000000
+vt 0.563776 0.000000
+vt 0.566327 0.000000
+vt 0.568878 0.000000
+vt 0.571429 0.000000
+vt 0.573980 0.000000
+vt 0.576531 0.000000
+vt 0.579082 0.000000
+vt 0.303571 0.000000
+vt 0.586735 0.000000
+vt 0.584184 0.000000
+vt 0.581633 0.000000
+vt 0.742347 0.000000
+vt 0.744898 0.000000
+vt 0.747449 0.000000
+vt 0.750000 0.000000
+vt 0.752551 0.000000
+vt 0.755102 0.000000
+vt 0.757653 0.000000
+vt 0.760204 0.000000
+vt 0.762755 0.000000
+vt 0.765306 0.000000
+vt 0.767857 0.000000
+vt 0.770408 0.000000
+vt 0.772959 0.000000
+vt 0.775510 0.000000
+vt 0.778061 0.000000
+vt 0.913265 0.000000
+vt 0.780612 0.000000
+vt 0.783163 0.000000
+vt 0.785714 0.000000
+vt 0.788265 0.000000
+vt 0.790816 0.000000
+vt 0.793367 0.000000
+vt 0.795918 0.000000
+vt 0.798469 0.000000
+vt 0.801020 0.000000
+vt 0.803571 0.000000
+vt 0.808673 0.000000
+vt 0.806122 0.000000
+vt 0.910714 0.000000
+vt 0.908163 0.000000
+vt 0.811224 0.000000
+vt 0.905612 0.000000
+vt 0.813776 0.000000
+vt 0.903061 0.000000
+vt 0.816327 0.000000
+vt 0.900510 0.000000
+vt 0.818878 0.000000
+vt 0.897959 0.000000
+vt 0.821429 0.000000
+vt 0.895408 0.000000
+vt 0.823980 0.000000
+vt 0.892857 0.000000
+vt 0.826531 0.000000
+vt 0.890306 0.000000
+vt 0.829082 0.000000
+vt 0.887755 0.000000
+vt 0.831633 0.000000
+vt 0.885204 0.000000
+vt 0.834184 0.000000
+vt 0.882653 0.000000
+vt 0.836735 0.000000
+vt 0.880102 0.000000
+vt 0.839286 0.000000
+vt 0.841837 0.000000
+vt 0.877551 0.000000
+vt 0.844388 0.000000
+vt 0.846939 0.000000
+vt 0.849490 0.000000
+vt 0.852041 0.000000
+vt 0.854592 0.000000
+vt 0.857143 0.000000
+vt 0.859694 0.000000
+vt 0.862245 0.000000
+vt 0.864796 0.000000
+vt 0.867347 0.000000
+vt 0.869898 0.000000
+vt 0.872449 0.000000
+vt 0.875000 0.000000
+s 0
+usemtl 
+f 29/1/1 27/2/1 28/3/1
+f 29/1/1 26/4/1 27/2/1
+f 29/1/1 25/5/1 26/4/1
+f 30/6/1 25/5/1 29/1/1
+f 30/6/1 24/7/1 25/5/1
+f 30/6/1 23/8/1 24/7/1
+f 30/6/1 22/9/1 23/8/1
+f 30/6/1 21/10/1 22/9/1
+f 30/6/1 20/11/1 21/10/1
+f 30/6/1 19/12/1 20/11/1
+f 30/6/1 18/13/1 19/12/1
+f 30/6/1 17/14/1 18/13/1
+f 30/6/1 16/15/1 17/14/1
+f 30/6/1 15/16/1 16/15/1
+f 30/6/1 14/17/1 15/16/1
+f 31/18/1 14/17/1 30/6/1
+f 31/18/1 13/19/1 14/17/1
+f 31/18/1 12/20/1 13/19/1
+f 32/21/1 12/20/1 31/18/1
+f 32/21/1 11/22/1 12/20/1
+f 33/23/1 11/22/1 32/21/1
+f 33/23/1 10/24/1 11/22/1
+f 34/25/1 10/24/1 33/23/1
+f 34/25/1 9/26/1 10/24/1
+f 35/27/1 9/26/1 34/25/1
+f 35/27/1 8/28/1 9/26/1
+f 36/29/1 8/28/1 35/27/1
+f 36/29/1 7/30/1 8/28/1
+f 37/31/1 7/30/1 36/29/1
+f 37/31/1 6/32/1 7/30/1
+f 38/33/1 6/32/1 37/31/1
+f 38/33/1 5/34/1 6/32/1
+f 39/35/1 5/34/1 38/33/1
+f 39/35/1 4/36/1 5/34/1
+f 40/37/1 4/36/1 39/35/1
+f 40/37/1 3/38/1 4/36/1
+f 41/39/1 3/38/1 40/37/1
+f 41/39/1 2/40/1 3/38/1
+f 42/41/1 2/40/1 41/39/1
+f 42/41/1 1/42/1 2/40/1
+f 43/43/1 1/42/1 42/41/1
+f 43/43/1 119/44/1 1/42/1
+f 43/43/1 118/45/1 119/44/1
+f 43/43/1 117/46/1 118/45/1
+f 43/43/1 116/47/1 117/46/1
+f 43/43/1 115/48/1 116/47/1
+f 43/43/1 114/49/1 115/48/1
+f 43/43/1 113/50/1 114/49/1
+f 43/43/1 112/51/1 113/50/1
+f 43/43/1 111/52/1 112/51/1
+f 43/43/1 110/53/1 111/52/1
+f 43/43/1 109/54/1 110/53/1
+f 43/43/1 108/55/1 109/54/1
+f 43/43/1 106/56/1 108/55/1
+f 106/56/1 107/57/1 108/55/1
+f 43/43/1 105/58/1 106/56/1
+f 44/59/1 105/58/1 43/43/1
+f 44/59/1 104/60/1 105/58/1
+f 44/59/1 103/61/1 104/60/1
+f 44/59/1 102/62/1 103/61/1
+f 44/59/1 101/63/1 102/62/1
+f 44/59/1 100/64/1 101/63/1
+f 44/59/1 99/65/1 100/64/1
+f 44/59/1 98/66/1 99/65/1
+f 44/59/1 97/67/1 98/66/1
+f 44/59/1 96/68/1 97/67/1
+f 44/59/1 95/69/1 96/68/1
+f 45/70/1 81/71/1 44/59/1
+f 81/71/1 95/69/1 44/59/1
+f 81/71/1 90/72/1 95/69/1
+f 90/72/1 91/73/1 95/69/1
+f 91/73/1 92/74/1 95/69/1
+f 92/74/1 93/75/1 95/69/1
+f 93/75/1 94/76/1 95/69/1
+f 81/71/1 89/77/1 90/72/1
+f 81/71/1 88/78/1 89/77/1
+f 81/71/1 87/79/1 88/78/1
+f 81/71/1 86/80/1 87/79/1
+f 81/71/1 85/81/1 86/80/1
+f 81/71/1 84/82/1 85/81/1
+f 81/71/1 83/83/1 84/82/1
+f 81/71/1 82/84/1 83/83/1
+f 45/70/1 80/85/1 81/71/1
+f 176/86/1 174/87/1 175/88/1
+f 176/86/1 173/89/1 174/87/1
+f 176/86/1 172/90/1 173/89/1
+f 177/91/1 172/90/1 176/86/1
+f 177/91/1 171/92/1 172/90/1
+f 177/91/1 170/93/1 171/92/1
+f 177/91/1 169/94/1 170/93/1
+f 177/91/1 168/95/1 169/94/1
+f 177/91/1 167/96/1 168/95/1
+f 177/91/1 166/97/1 167/96/1
+f 177/91/1 165/98/1 166/97/1
+f 177/91/1 164/99/1 165/98/1
+f 177/91/1 163/100/1 164/99/1
+f 177/91/1 162/101/1 163/100/1
+f 177/91/1 161/102/1 162/101/1
+f 177/91/1 160/103/1 161/102/1
+f 178/104/1 160/103/1 177/91/1
+f 178/104/1 159/105/1 160/103/1
+f 179/106/1 159/105/1 178/104/1
+f 179/106/1 158/107/1 159/105/1
+f 180/108/1 158/107/1 179/106/1
+f 180/108/1 157/109/1 158/107/1
+f 181/110/1 157/109/1 180/108/1
+f 181/110/1 156/111/1 157/109/1
+f 182/112/1 156/111/1 181/110/1
+f 182/112/1 155/113/1 156/111/1
+f 183/114/1 155/113/1 182/112/1
+f 183/114/1 154/115/1 155/113/1
+f 184/116/1 154/115/1 183/114/1
+f 184/116/1 153/117/1 154/115/1
+f 46/118/1 80/85/1 45/70/1
+f 185/119/1 153/117/1 184/116/1
+f 185/119/1 152/120/1 153/117/1
+f 186/121/1 152/120/1 185/119/1
+f 186/121/1 151/122/1 152/120/1
+f 187/123/1 151/122/1 186/121/1
+f 187/123/1 150/124/1 151/122/1
+f 188/125/1 150/124/1 187/123/1
+f 188/125/1 149/126/1 150/124/1
+f 189/127/1 149/126/1 188/125/1
+f 189/127/1 148/128/1 149/126/1
+f 190/129/1 148/128/1 189/127/1
+f 190/129/1 257/130/1 148/128/1
+f 257/130/1 146/131/1 148/128/1
+f 146/131/1 147/132/1 148/128/1
+f 257/130/1 258/133/1 146/131/1
+f 258/133/1 145/134/1 146/131/1
+f 190/129/1 256/135/1 257/130/1
+f 259/136/1 145/134/1 258/133/1
+f 191/137/1 256/135/1 190/129/1
+f 191/137/1 255/138/1 256/135/1
+f 191/137/1 254/139/1 255/138/1
+f 191/137/1 253/140/1 254/139/1
+f 191/137/1 252/141/1 253/140/1
+f 259/136/1 144/142/1 145/134/1
+f 191/137/1 251/143/1 252/141/1
+f 191/137/1 250/144/1 251/143/1
+f 191/137/1 249/145/1 250/144/1
+f 191/137/1 248/146/1 249/145/1
+f 191/137/1 247/147/1 248/146/1
+f 191/137/1 246/148/1 247/147/1
+f 192/149/1 232/150/1 191/137/1
+f 232/150/1 246/148/1 191/137/1
+f 232/150/1 241/151/1 246/148/1
+f 241/151/1 242/152/1 246/148/1
+f 242/152/1 243/153/1 246/148/1
+f 243/153/1 244/154/1 246/148/1
+f 244/154/1 245/155/1 246/148/1
+f 232/150/1 240/156/1 241/151/1
+f 232/150/1 239/157/1 240/156/1
+f 232/150/1 238/158/1 239/157/1
+f 232/150/1 237/159/1 238/158/1
+f 232/150/1 236/160/1 237/159/1
+f 232/150/1 235/161/1 236/160/1
+f 232/150/1 234/162/1 235/161/1
+f 259/136/1 143/163/1 144/142/1
+f 232/150/1 233/164/1 234/162/1
+f 47/165/1 80/85/1 46/118/1
+f 192/149/1 286/166/1 232/150/1
+f 192/149/1 285/167/1 286/166/1
+f 291/168/1 289/169/1 290/170/1
+f 291/168/1 288/171/1 289/169/1
+f 291/168/1 287/172/1 288/171/1
+f 383/173/1 381/174/1 382/175/1
+f 384/176/1 381/174/1 383/173/1
+f 385/177/1 381/174/1 384/176/1
+f 386/178/1 381/174/1 385/177/1
+f 387/179/1 381/174/1 386/178/1
+f 388/180/1 381/174/1 387/179/1
+f 389/181/1 381/174/1 388/180/1
+f 390/182/1 381/174/1 389/181/1
+f 391/183/1 381/174/1 390/182/1
+f 392/184/1 381/174/1 391/183/1
+f 393/185/1 381/174/1 392/184/1
+f 287/172/1 381/174/1 393/185/1
+f 291/168/1 381/174/1 287/172/1
+f 291/168/1 380/186/1 381/174/1
+f 291/168/1 379/187/1 380/186/1
+f 291/168/1 378/188/1 379/187/1
+f 291/168/1 377/189/1 378/188/1
+f 193/190/1 285/167/1 192/149/1
+f 291/168/1 376/191/1 377/189/1
+f 291/168/1 375/192/1 376/191/1
+f 291/168/1 374/193/1 375/192/1
+f 291/168/1 373/194/1 374/193/1
+f 291/168/1 372/195/1 373/194/1
+f 291/168/1 371/196/1 372/195/1
+f 291/168/1 370/197/1 371/196/1
+f 291/168/1 369/198/1 370/197/1
+f 259/136/1 142/199/1 143/163/1
+f 291/168/1 368/200/1 369/198/1
+f 48/201/1 80/85/1 47/165/1
+f 291/168/1 367/202/1 368/200/1
+f 194/203/1 285/167/1 193/190/1
+f 259/136/1 141/204/1 142/199/1
+f 291/168/1 366/205/1 367/202/1
+f 49/206/1 80/85/1 48/201/1
+f 195/207/1 285/167/1 194/203/1
+f 259/136/1 140/208/1 141/204/1
+f 291/168/1 365/209/1 366/205/1
+f 50/210/1 80/85/1 49/206/1
+f 196/211/1 285/167/1 195/207/1
+f 259/136/1 139/212/1 140/208/1
+f 291/168/1 364/213/1 365/209/1
+f 51/214/1 80/85/1 50/210/1
+f 197/215/1 285/167/1 196/211/1
+f 259/136/1 138/216/1 139/212/1
+f 291/168/1 363/217/1 364/213/1
+f 52/218/1 80/85/1 51/214/1
+f 198/219/1 285/167/1 197/215/1
+f 259/136/1 137/220/1 138/216/1
+f 53/221/1 80/85/1 52/218/1
+f 291/168/1 362/222/1 363/217/1
+f 54/223/1 55/224/1 53/221/1
+f 55/224/1 56/225/1 53/221/1
+f 56/225/1 57/226/1 53/221/1
+f 57/226/1 58/227/1 53/221/1
+f 58/227/1 59/228/1 53/221/1
+f 59/228/1 60/229/1 53/221/1
+f 60/229/1 61/230/1 53/221/1
+f 61/230/1 62/231/1 53/221/1
+f 62/231/1 63/232/1 53/221/1
+f 63/232/1 78/233/1 53/221/1
+f 78/233/1 80/85/1 53/221/1
+f 199/234/1 285/167/1 198/219/1
+f 259/136/1 136/235/1 137/220/1
+f 64/236/1 78/233/1 63/232/1
+f 65/237/1 78/233/1 64/236/1
+f 66/238/1 67/239/1 65/237/1
+f 67/239/1 70/240/1 65/237/1
+f 70/240/1 75/241/1 65/237/1
+f 75/241/1 76/242/1 65/237/1
+f 76/242/1 77/243/1 65/237/1
+f 77/243/1 78/233/1 65/237/1
+f 68/244/1 69/245/1 67/239/1
+f 69/245/1 70/240/1 67/239/1
+f 71/246/1 72/247/1 70/240/1
+f 72/247/1 73/248/1 70/240/1
+f 73/248/2 75/241/2 70/240/2
+f 74/249/1 75/241/1 73/248/1
+f 79/250/1 80/85/1 78/233/1
+f 260/251/1 136/235/1 259/136/1
+f 291/168/1 361/252/1 362/222/1
+f 200/253/1 285/167/1 199/234/1
+f 261/254/1 136/235/1 260/251/1
+f 262/255/1 136/235/1 261/254/1
+f 263/256/1 136/235/1 262/255/1
+f 264/257/1 136/235/1 263/256/1
+f 265/258/1 136/235/1 264/257/1
+f 266/259/1 136/235/1 265/258/1
+f 267/260/1 136/235/1 266/259/1
+f 268/261/1 136/235/1 267/260/1
+f 269/262/1 136/235/1 268/261/1
+f 270/263/1 136/235/1 269/262/1
+f 271/264/1 136/235/1 270/263/1
+f 272/265/1 136/235/1 271/264/1
+f 273/266/1 136/235/1 272/265/1
+f 274/267/1 136/235/1 273/266/1
+f 275/268/1 136/235/1 274/267/1
+f 275/268/1 135/269/1 136/235/1
+f 276/270/1 135/269/1 275/268/1
+f 277/271/1 135/269/1 276/270/1
+f 278/272/1 135/269/1 277/271/1
+f 279/273/1 135/269/1 278/272/1
+f 280/274/1 135/269/1 279/273/1
+f 201/275/1 285/167/1 200/253/1
+f 281/276/1 135/269/1 280/274/1
+f 282/277/1 135/269/1 281/276/1
+f 283/278/1 135/269/1 282/277/1
+f 284/279/1 135/269/1 283/278/1
+f 203/280/1 201/275/1 202/281/1
+f 203/280/1 285/167/1 201/275/1
+f 203/280/1 284/279/1 285/167/1
+f 203/280/1 135/269/1 284/279/1
+f 203/280/1 134/282/1 135/269/1
+f 203/280/1 133/283/1 134/282/1
+f 204/284/1 133/283/1 203/280/1
+f 204/284/1 132/285/1 133/283/1
+f 205/286/1 132/285/1 204/284/1
+f 205/286/1 131/287/1 132/285/1
+f 206/288/1 131/287/1 205/286/1
+f 206/288/1 130/289/1 131/287/1
+f 207/290/1 130/289/1 206/288/1
+f 207/290/1 129/291/1 130/289/1
+f 208/292/1 129/291/1 207/290/1
+f 208/292/1 128/293/1 129/291/1
+f 209/294/1 128/293/1 208/292/1
+f 291/168/1 360/295/1 361/252/1
+f 209/294/1 127/296/1 128/293/1
+f 210/297/1 127/296/1 209/294/1
+f 210/297/1 126/298/1 127/296/1
+f 211/299/1 126/298/1 210/297/1
+f 211/299/1 125/300/1 126/298/1
+f 212/301/1 125/300/1 211/299/1
+f 212/301/1 124/302/1 125/300/1
+f 213/303/1 124/302/1 212/301/1
+f 213/303/1 123/304/1 124/302/1
+f 214/305/1 123/304/1 213/303/1
+f 214/305/1 122/306/1 123/304/1
+f 215/307/1 122/306/1 214/305/1
+f 216/308/1 122/306/1 215/307/1
+f 216/308/1 121/309/1 122/306/1
+f 217/310/1 121/309/1 216/308/1
+f 218/311/1 121/309/1 217/310/1
+f 219/312/1 121/309/1 218/311/1
+f 220/313/1 121/309/1 219/312/1
+f 221/314/1 121/309/1 220/313/1
+f 222/315/1 121/309/1 221/314/1
+f 223/316/1 121/309/1 222/315/1
+f 224/317/1 121/309/1 223/316/1
+f 225/318/1 121/309/1 224/317/1
+f 226/319/1 121/309/1 225/318/1
+f 227/320/1 121/309/1 226/319/1
+f 228/321/1 121/309/1 227/320/1
+f 228/321/1 120/322/1 121/309/1
+f 228/321/1 231/323/1 120/322/1
+f 228/321/1 230/324/1 231/323/1
+f 229/325/1 230/324/1 228/321/1
+f 292/326/1 360/295/1 291/168/1
+f 293/327/1 360/295/1 292/326/1
+f 294/328/1 360/295/1 293/327/1
+f 295/329/1 360/295/1 294/328/1
+f 296/330/1 360/295/1 295/329/1
+f 297/331/1 360/295/1 296/330/1
+f 298/332/1 360/295/1 297/331/1
+f 299/333/1 360/295/1 298/332/1
+f 300/334/1 360/295/1 299/333/1
+f 301/335/1 360/295/1 300/334/1
+f 302/336/1 360/295/1 301/335/1
+f 303/337/1 360/295/1 302/336/1
+f 304/338/1 360/295/1 303/337/1
+f 305/339/1 360/295/1 304/338/1
+f 306/340/1 360/295/1 305/339/1
+f 306/340/1 359/341/1 360/295/1
+f 307/342/1 359/341/1 306/340/1
+f 308/343/1 359/341/1 307/342/1
+f 309/344/1 359/341/1 308/343/1
+f 310/345/1 359/341/1 309/344/1
+f 311/346/1 359/341/1 310/345/1
+f 312/347/1 359/341/1 311/346/1
+f 313/348/1 359/341/1 312/347/1
+f 314/349/1 359/341/1 313/348/1
+f 315/350/1 359/341/1 314/349/1
+f 316/351/1 359/341/1 315/350/1
+f 318/352/1 316/351/1 317/353/1
+f 318/352/1 359/341/1 316/351/1
+f 318/352/1 358/354/1 359/341/1
+f 318/352/1 357/355/1 358/354/1
+f 319/356/1 357/355/1 318/352/1
+f 319/356/1 356/357/1 357/355/1
+f 320/358/1 356/357/1 319/356/1
+f 320/358/1 355/359/1 356/357/1
+f 321/360/1 355/359/1 320/358/1
+f 321/360/1 354/361/1 355/359/1
+f 322/362/1 354/361/1 321/360/1
+f 322/362/1 353/363/1 354/361/1
+f 323/364/1 353/363/1 322/362/1
+f 323/364/1 352/365/1 353/363/1
+f 324/366/1 352/365/1 323/364/1
+f 324/366/1 351/367/1 352/365/1
+f 325/368/1 351/367/1 324/366/1
+f 325/368/1 350/369/1 351/367/1
+f 326/370/1 350/369/1 325/368/1
+f 326/370/1 349/371/1 350/369/1
+f 327/372/1 349/371/1 326/370/1
+f 327/372/1 348/373/1 349/371/1
+f 328/374/1 348/373/1 327/372/1
+f 328/374/1 347/375/1 348/373/1
+f 329/376/1 347/375/1 328/374/1
+f 329/376/1 346/377/1 347/375/1
+f 330/378/1 346/377/1 329/376/1
+f 331/379/1 346/377/1 330/378/1
+f 331/379/1 345/380/1 346/377/1
+f 332/381/1 345/380/1 331/379/1
+f 333/382/1 345/380/1 332/381/1
+f 334/383/1 345/380/1 333/382/1
+f 335/384/1 345/380/1 334/383/1
+f 336/385/1 345/380/1 335/384/1
+f 337/386/1 345/380/1 336/385/1
+f 338/387/1 345/380/1 337/386/1
+f 339/388/1 345/380/1 338/387/1
+f 340/389/1 345/380/1 339/388/1
+f 341/390/1 345/380/1 340/389/1
+f 342/391/1 345/380/1 341/390/1
+f 343/392/1 345/380/1 342/391/1
+f 343/392/1 344/393/1 345/380/1

+ 3 - 4
bin/app/src/app/locale.rs

@@ -22,10 +22,9 @@ use std::sync::mpsc::sync_channel;
 mod ui_consts {
     pub const LOCALE_PATH: &str = "lang/{locale}/{entry}";
 }
-#[cfg(all(
-    any(target_os = "linux", target_os = "macos", target_os = "windows"),
-    not(feature = "emulate-android")
-))]
+#[cfg(
+    any(target_os = "linux", target_os = "macos", target_os = "windows")
+)]
 mod ui_consts {
     pub const LOCALE_PATH: &str = "assets/lang/{locale}/{entry}";
 }

+ 13 - 0
bin/app/src/app/node.rs

@@ -222,12 +222,20 @@ pub fn create_text(name: &str) -> SceneNode {
     prop.allow_exprs();
     node.add_property(prop).unwrap();
 
+    let mut prop = Property::new("height", PropertyType::Float32, PropertySubType::Pixel);
+    node.add_property(prop).unwrap();
+
     let mut prop = Property::new("lineheight", PropertyType::Float32, PropertySubType::Pixel);
     prop.set_ui_text("Line Height", "Line height/lead (em)");
     prop.set_defaults_f32(vec![1.2]).unwrap();
     prop.set_range_f32(0., f32::MAX);
     node.add_property(prop).unwrap();
 
+    let mut prop = Property::new("text_align", PropertyType::Enum, PropertySubType::Null);
+    prop.set_enum_items(vec!["start", "end", "left", "center", "right", "justify"]).unwrap();
+    prop.set_defaults_str(vec!["start".to_string()]).unwrap();
+    node.add_property(prop).unwrap();
+
     let prop = Property::new("font_size", PropertyType::Float32, PropertySubType::Pixel);
     node.add_property(prop).unwrap();
 
@@ -239,6 +247,11 @@ pub fn create_text(name: &str) -> SceneNode {
     prop.set_range_f32(0., 1.);
     node.add_property(prop).unwrap();
 
+    let mut prop = Property::new("overflow_wrap", PropertyType::Enum, PropertySubType::Null);
+    prop.set_enum_items(vec!["normal", "anywhere", "break-word"]).unwrap();
+    prop.set_defaults_str(vec!["normal".to_string()]).unwrap();
+    node.add_property(prop).unwrap();
+
     let prop = Property::new("z_index", PropertyType::Uint32, PropertySubType::Null);
     node.add_property(prop).unwrap();
 

+ 87 - 7
bin/app/src/app/schema/menu/mod.rs

@@ -265,11 +265,12 @@ pub async fn make(app: &App, content: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let renderer = app.renderer.clone();
     let contact_is_visible =
         PropertyBool::wrap(&contact_layer, Role::App, "is_visible", 0).unwrap();
+    let menulayer_is_visible1 = menulayer_is_visible.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             let atom = &mut renderer.make_guard(gfxtag!("write_click"));
             contact_is_visible.set(atom, true);
-            menulayer_is_visible.set(atom, false);
+            menulayer_is_visible1.set(atom, false);
         }
     });
     app.tasks.lock().unwrap().push(listen_click);
@@ -413,12 +414,12 @@ pub async fn make(app: &App, content: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         while let Ok(data) = recvr.recv().await {
             let channel: String = deserialize(&data).unwrap();
             let path = format!("/window/content/{}_chat_layer", channel);
-            let node = sg_root.lookup_node(path).unwrap();
-
-            let atom = &mut renderer.make_guard(gfxtag!("channel_clicked"));
-            info!(target: "app::menu", "clicked: {channel}!");
-            node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
-            menu_is_visible.set(atom, false);
+            if let Some(node) = sg_root.lookup_node(path) {
+                let atom = &mut renderer.make_guard(gfxtag!("channel_clicked"));
+                info!(target: "app::menu", "clicked: {channel}!");
+                node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+                menu_is_visible.set(atom, false);
+            }
         }
     });
     app.tasks.lock().unwrap().push(listen_click);
@@ -518,3 +519,82 @@ pub async fn make(app: &App, content: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
     editlayer_node.link(node);
 }
+
+pub async fn setup_wallet_button(app: &App, menu_layer: SceneNodePtr, i18n_fish: &I18nBabelFish) {
+    let atom = &mut PropertyAtomicGuard::none();
+    let mut cc = expr::Compiler::new();
+
+    let window_scale = PropertyFloat32::wrap(
+        &app.sg_root.lookup_node("/window").unwrap(),
+        Role::Internal,
+        "scale",
+        0,
+    )
+    .unwrap();
+
+    let menulayer_is_visible = PropertyBool::wrap(&menu_layer, Role::App, "is_visible", 0).unwrap();
+
+    let mainlayer_node = app.sg_root.lookup_node("/window/content/menu_layer/mainbtn_layer").unwrap();
+
+    // Wallet button
+    let node = create_vector_art("walletbtn_bg");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - 260").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    let code = cc.compile("h - 150").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_f32(atom, Role::App, 2, 100.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 100.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+
+    let mut shape = VectorShape::new();
+
+    shape.add_gradient_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [[0., 0.1, 0.15, 1.], [0., 0.1, 0.15, 1.], [0., 0., 0., 1.], [0., 0., 0., 1.]],
+    );
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        [0., 0.94, 1., 1.],
+    );
+
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    mainlayer_node.link(node);
+
+    let node = create_button("wallet_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - 260").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    let code = cc.compile("h - 150").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_f32(atom, Role::App, 2, 100.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 100.).unwrap();
+
+    let (slot, recvr) = Slot::new("wallet_clicked");
+    node.register("click", slot).unwrap();
+    let sg_root = app.sg_root.clone();
+    let wallet_is_visible =
+        PropertyBool::wrap(&sg_root.lookup_node("/window/content/wallet_main_layer").unwrap(), Role::App, "is_visible", 0).unwrap();
+    let renderer = app.renderer.clone();
+    let menulayer_is_visible2 = menulayer_is_visible.clone();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("wallet_click"));
+            wallet_is_visible.set(atom, true);
+            menulayer_is_visible2.set(atom, false);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let renderer = app.renderer.clone();
+    let node = node.setup(|me| Button::new(me, renderer)).await;
+    mainlayer_node.link(node);
+}

+ 6 - 0
bin/app/src/app/schema/mod.rs

@@ -38,6 +38,7 @@ use crate::{
 mod chat;
 mod menu;
 //mod settings;
+mod wallet;
 pub mod test;
 pub mod test_scroll_layer;
 
@@ -602,6 +603,11 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
         .await;
     }
     menu::make(app, content.clone(), i18n_fish).await;
+    wallet::make(app, content.clone(), i18n_fish).await;
+
+    // Setup wallet button after wallet layer is created
+    let menu_layer = app.sg_root.lookup_node("/window/content/menu_layer").unwrap();
+    menu::setup_wallet_button(app, menu_layer, i18n_fish).await;
 
     // @@@ Debug stuff @@@
     //let chatview_node = app.sg_root.lookup_node("/window/content/dev_chat_layer").unwrap();

+ 2 - 2
bin/app/src/app/schema/settings.rs

@@ -141,9 +141,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     cc.add_const_f32("SEARCH_PADDING_X", SEARCH_PADDING_X);
     cc.add_const_f32("X_RATIO", 1. / 2.);
     let window_scale = PropertyFloat32::wrap(
-        &app.sg_root.lookup_node("/setting/scale").unwrap(),
+        &app.sg_root.lookup_node("/window").unwrap(),
         Role::Internal,
-        "value",
+        "scale",
         0,
     )
     .unwrap();

+ 2 - 2
bin/app/src/app/schema/test.rs

@@ -66,9 +66,9 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     let atom = &mut PropertyAtomicGuard::none();
 
     let window_scale = PropertyFloat32::wrap(
-        &app.sg_root.lookup_node("/setting/scale").unwrap(),
+        &app.sg_root.lookup_node("/window").unwrap(),
         Role::Internal,
-        "value",
+        "scale",
         0,
     )
     .unwrap();

+ 2857 - 0
bin/app/src/app/schema/wallet.rs

@@ -0,0 +1,2857 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi::system::msleep;
+use super::{ColorScheme, COLOR_SCHEME};
+use std::pin::Pin;
+use std::sync::{Arc, Mutex};
+use crate::{
+    app::{
+        App, node::{
+            create_button, create_layer, create_menu, create_singleline_edit, create_text,
+            create_vector_art,
+        }
+    },
+    expr,
+    gfx::gfxtag,
+    mesh::{COLOR_CYAN, COLOR_TEAL},
+    prop::{PropertyAtomicGuard, PropertyBool, PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr, Role},
+    scene::{Pimpl, SceneNode, SceneNodePtr, SceneNodeType, Slot},
+    shape,
+    ui::{BaseEdit, BaseEditType, Button, Layer, Menu, ShapeVertex, Shortcut, Text, VectorArt, VectorShape},
+    util::i18n::I18nBabelFish,
+};
+
+#[cfg(any(target_os = "android", feature = "emulate-android"))]
+mod android_ui_consts {
+    pub const BACKARROW_SCALE: f32 = 30.;
+    pub const BACKARROW_X: f32 = 50.;
+    pub const BACKARROW_Y: f32 = 70.;
+    pub const TITLE_FONTSIZE: f32 = 56.;
+    pub const BUTTON_FONTSIZE: f32 = 48.;
+    pub const BASE_FONTSIZE: f32 = 48.;
+    pub const HINT_FONTSIZE: f32 = BASE_FONTSIZE * 0.8;
+    pub const AMOUNT_FONTSIZE: f32 = 118.;
+    pub const AMOUNT_CHAR_WIDTH: f32 = AMOUNT_FONTSIZE * 0.6;
+    pub const AMOUNT_TOKEN_SPACING: f32 = AMOUNT_FONTSIZE * 0.35;
+    pub const BUTTON_HEIGHT: f32 = 200.;
+    pub const TOKEN_ROW_HEIGHT: f32 = 80.;
+    pub const TITLE_PADDING: f32 = 50.;
+    pub const TOKEN_NAME_OFFSET: f32 = 200.;
+    pub const PADDING_X: f32 = 40.;
+    pub const PADDING_Y: f32 = 30.;
+    pub const RECIPIENT_INPUT_MARGIN: f32 = 30.;
+    pub const RECIPIENT_INPUT_PADDING_X: f32 = 40.;
+    pub const RECIPIENT_INPUT_HEIGHT: f32 = 120.;
+    pub const RECIPIENT_INPUT_FONTSIZE: f32 = 48.;
+    pub const HEADER_HEIGHT: f32 = 140.;
+    pub const ROW_HEIGHT: f32 = 80.;
+    pub const WALLET_BTN_SIZE: f32 = 50.;
+    pub const COPY_WIDTH: f32 = 200.;
+    pub const COPY_SCALE: f32 = 30.;
+}
+
+#[cfg(target_os = "android")]
+mod ui_consts {
+    pub use super::android_ui_consts::*;
+}
+
+#[cfg(feature = "emulate-android")]
+mod ui_consts {
+    pub use super::android_ui_consts::*;
+}
+
+#[cfg(all(
+    any(target_os = "linux", target_os = "macos", target_os = "windows"),
+    not(feature = "emulate-android")
+))]
+mod ui_consts {
+    pub const BACKARROW_SCALE: f32 = 15.;
+    pub const BACKARROW_X: f32 = 38.;
+    pub const BACKARROW_Y: f32 = 26.;
+    pub const TITLE_FONTSIZE: f32 = 20.;
+    pub const BUTTON_FONTSIZE: f32 = 20.;
+    pub const BASE_FONTSIZE: f32 = 20.;
+    pub const HINT_FONTSIZE: f32 = BASE_FONTSIZE * 0.8;
+    pub const AMOUNT_FONTSIZE: f32 = 56.;
+    pub const AMOUNT_CHAR_WIDTH: f32 = AMOUNT_FONTSIZE * 0.6;
+    pub const AMOUNT_TOKEN_SPACING: f32 = AMOUNT_FONTSIZE * 0.35;
+    pub const BUTTON_HEIGHT: f32 = 90.;
+    pub const TOKEN_ROW_HEIGHT: f32 = 40.;
+    pub const TITLE_PADDING: f32 = 25.;
+    pub const TOKEN_NAME_OFFSET: f32 = 90.;
+    pub const PADDING_X: f32 = 20.;
+    pub const PADDING_Y: f32 = 15.;
+    pub const RECIPIENT_INPUT_MARGIN: f32 = 15.;
+    pub const RECIPIENT_INPUT_PADDING_X: f32 = 20.;
+    pub const RECIPIENT_INPUT_HEIGHT: f32 = 60.;
+    pub const RECIPIENT_INPUT_FONTSIZE: f32 = 20.;
+    pub const HEADER_HEIGHT: f32 = 60.;
+    pub const ROW_HEIGHT: f32 = 80.;
+    pub const WALLET_BTN_SIZE: f32 = 50.;
+    pub const COPY_WIDTH: f32 = 100.;
+    pub const COPY_SCALE: f32 = 15.;
+}
+
+use ui_consts::*;
+
+const MOCK_TOKENS: &[(&str, &str, f32)] = &[
+    ("DRK", "Dark", 100.24),
+    ("wXMR", "wrapped Monero", 5.56487),
+    ("wBTC", "wrapped Bitcoin", 0.78956413),
+    ("RNDM", "Random", 0.78956413),
+    ("OTHER", "Other token", 0.78956413),
+    ("OTHER", "Other token", 0.78956413),
+    ("OTHER", "Other token", 0.78956413),
+    ("OTHER", "Other token", 0.78956413),
+];
+
+const MOCK_RECEIVE_ADDRESS: &str = "8siycy4q5rsf3rwxa9ptw5v7syfj1m1ov27tt78rj7kprth1ux41lqpvut6cht7w";
+const MOCK_TX_FEE: &str = "0.001 DRK";
+
+fn is_valid_address(address: &str) -> bool {
+    address.len() > 3 // TODO
+}
+
+fn get_balance(token_symbol: &str) -> f32 {
+    MOCK_TOKENS
+        .iter()
+        .find(|(symbol, _, _)| *symbol == token_symbol)
+        .map(|(_, _, balance)| *balance)
+        .unwrap_or(0.0)
+}
+
+// Send transaction data shared across all send pages
+#[derive(Debug, Clone)]
+struct SendTxData {
+    token_symbol: Option<String>,
+    token_name: Option<String>,
+    recipient: Option<String>,
+    amount: Option<String>,
+}
+
+impl SendTxData {
+    fn new() -> Self {
+        Self {
+            token_symbol: None,
+            token_name: None,
+            recipient: None,
+            amount: None,
+        }
+    }
+}
+
+pub async fn make(app: &App, content: SceneNodePtr, i18n_fish: &I18nBabelFish) {
+    let window_scale = PropertyFloat32::wrap(
+        &app.sg_root.lookup_node("/window").unwrap(),
+        Role::Internal,
+        "scale",
+        0,
+    )
+    .unwrap();
+
+    let mut cc = expr::Compiler::new();
+    cc.add_const_f32("HEADER_HEIGHT", HEADER_HEIGHT);
+    cc.add_const_f32("TOKEN_ROW_HEIGHT", TOKEN_ROW_HEIGHT);
+    cc.add_const_f32("PADDING_X", PADDING_X);
+    cc.add_const_f32("PADDING_Y", PADDING_Y);
+    cc.add_const_f32("TITLE_FONTSIZE", TITLE_FONTSIZE);
+    cc.add_const_f32("BUTTON_HEIGHT", BUTTON_HEIGHT);
+    cc.add_const_f32("BUTTON_FONTSIZE", BUTTON_FONTSIZE);
+    cc.add_const_f32("ROW_HEIGHT", ROW_HEIGHT);
+
+    let atom = &mut PropertyAtomicGuard::none();
+
+    // ============================================
+    // Main Wallet Layer
+    // ============================================
+    let wallet_layer = create_layer("wallet_main_layer");
+    let prop = wallet_layer.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    wallet_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    wallet_layer.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    let wallet_layer = wallet_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(wallet_layer.clone());
+
+    let wallet_is_visible = PropertyBool::wrap(&wallet_layer, Role::App, "is_visible", 0).unwrap();
+
+    create_bg_mesh(app, atom, &wallet_layer, "wallet_bg").await;
+    create_header_bg(app, atom, &wallet_layer, "wallet_header_bg").await;
+
+    // Back button
+    let node = create_vector_art("wallet_back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    wallet_layer.link(node);
+
+    let node = create_button("wallet_back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+
+    let sg_root = app.sg_root.clone();
+    let renderer = app.renderer.clone();
+    let menu_is_visible =
+        PropertyBool::wrap(&sg_root.lookup_node("/window/content/menu_layer").unwrap(), Role::App, "is_visible", 0).unwrap();
+    let wallet_is_visible1 = wallet_is_visible.clone();
+    let goback = async move || {
+        info!(target: "app::wallet", "clicked back from wallet");
+        let atom = &mut renderer.make_guard(gfxtag!("wallet goback action"));
+        wallet_is_visible1.set(atom, false);
+        menu_is_visible.set(atom, true);
+    };
+
+    let (slot, recvr) = Slot::new("wallet_back_clicked");
+    node.register("click", slot).unwrap();
+    let goback2 = goback.clone();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            goback2().await;
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    wallet_layer.link(node);
+
+    let mut y = HEADER_HEIGHT;
+
+    // Balance display
+    let node = create_text("wallet_balance");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + TITLE_PADDING).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "DRK 100.24").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    wallet_layer.link(node);
+
+    y += TITLE_PADDING * 2. + TITLE_FONTSIZE + 1.;
+
+    create_separator(app, atom, &wallet_layer, "wallet_balance_separator", &mut y).await;
+
+    // Receive button bg
+    let node = create_vector_art("wallet_receive_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_X).unwrap();
+    let code = cc.compile("w / 2 - PADDING_X * 1.5").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        COLOR_TEAL,
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    wallet_layer.link(node);
+
+    // Receive button click handler
+    let node = create_button("wallet_receive_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_X).unwrap();
+    let code = cc.compile("w / 2 - PADDING_X * 1.5").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+
+    let receive_layer = wallet_receive_layer(app, content.clone(), wallet_layer.clone(), i18n_fish, window_scale.clone()).await;
+    let receive_is_visible =
+        PropertyBool::wrap(&receive_layer, Role::App, "is_visible", 0).unwrap();
+    let renderer = app.renderer.clone();
+    let wallet_is_visible2 = wallet_is_visible.clone();
+    let (slot, recvr) = Slot::new("receive_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("receive button click"));
+            wallet_is_visible2.set(atom, false);
+            receive_is_visible.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    wallet_layer.link(node);
+
+    // Receive label
+    let node = create_text("wallet_receive_label");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("PADDING_X + (w / 2 - PADDING_X * 1.5) / 2 - (BUTTON_FONTSIZE * 0.6 * 7) / 2").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_X + BUTTON_HEIGHT / 2. - BUTTON_FONTSIZE / 1.8).unwrap();
+    let code = cc.compile("w / 2 - PADDING_X * 1.5").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "receive").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+        prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+        prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+        prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    wallet_layer.link(node);
+
+    // Send button bg
+    let node = create_vector_art("wallet_send_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("PADDING_X / 2 + w / 2").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_X).unwrap();
+    let code = cc.compile("w / 2 - PADDING_X * 1.5").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        COLOR_TEAL,
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    wallet_layer.link(node);
+
+    // Send button click handler
+    let node = create_button("wallet_send_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("PADDING_X / 2 + w / 2").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_X).unwrap();
+    let code = cc.compile("w / 2 - PADDING_X * 1.5").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+
+    let send_layer = wallet_send_layer(app, content.clone(), wallet_layer.clone(), i18n_fish, window_scale.clone()).await;
+    let send_is_visible = PropertyBool::wrap(&send_layer, Role::App, "is_visible", 0).unwrap();
+    let renderer = app.renderer.clone();
+    let wallet_is_visible3 = wallet_is_visible.clone();
+    let (slot, recvr) = Slot::new("send_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("send button click"));
+            wallet_is_visible3.set(atom, false);
+            send_is_visible.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    wallet_layer.link(node);
+
+    // Send label
+    let node = create_text("wallet_send_label");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("PADDING_X / 2 + w / 2 + (w / 2 - PADDING_X * 1.5) / 2 - (BUTTON_FONTSIZE * 0.6 * 4) / 2").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_X + BUTTON_HEIGHT / 2. - BUTTON_FONTSIZE / 1.8).unwrap();
+    let code = cc.compile("w / 2 - PADDING_X * 1.5").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "send").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+        prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+        prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+        prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    wallet_layer.link(node);
+
+    y += PADDING_X * 2. + BUTTON_HEIGHT + 1.;
+
+    create_separator(app, atom, &wallet_layer, "wallet_buttons_separator", &mut y).await;
+
+    create_title(app, atom, &wallet_layer, &window_scale, i18n_fish, "TOKENS", &mut y).await;
+
+    create_tokens_table(app, atom, &window_scale, i18n_fish, &wallet_layer, MOCK_TOKENS, &mut y, |_, _, _, _| {}).await;
+}
+
+async fn wallet_receive_layer(
+    app: &App,
+    content: SceneNodePtr,
+    wallet_layer: SceneNodePtr,
+    i18n_fish: &I18nBabelFish,
+    window_scale: PropertyFloat32,
+) -> SceneNodePtr {
+    let atom = &mut PropertyAtomicGuard::none();
+
+    let mut cc = expr::Compiler::new();
+    cc.add_const_f32("PADDING_Y", PADDING_Y);
+    cc.add_const_f32("COPY_WIDTH", COPY_WIDTH);
+
+    // Receive layer
+    let receive_layer = create_layer("wallet_receive_layer");
+    let prop = receive_layer.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    receive_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    receive_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let receive_layer = receive_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(receive_layer.clone());
+
+    create_bg_mesh(app, atom, &receive_layer, "receive_bg").await;
+    create_header_bg(app, atom, &receive_layer, "receive_header_bg").await;
+
+    // Back button
+    let node = create_vector_art("receive_back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    receive_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("receive_back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+
+    let wallet_is_visible = PropertyBool::wrap(&wallet_layer, Role::App, "is_visible", 0).unwrap();
+    let receive_is_visible = PropertyBool::wrap(&receive_layer, Role::App, "is_visible", 0).unwrap();
+    let renderer = app.renderer.clone();
+    let (slot, recvr) = Slot::new("receive_back_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("receive back button"));
+            receive_is_visible.set(atom, false);
+            wallet_is_visible.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    receive_layer.link(node);
+
+    y += HEADER_HEIGHT;
+
+    create_title(app, atom, &receive_layer, &window_scale, i18n_fish, "RECEIVE", &mut y).await;
+
+    create_subtitle(app, atom, &receive_layer, &window_scale, i18n_fish, "Address", &mut y).await;
+
+    // Address display
+    let node = create_text("receive_address");
+    let addr_h_prop = node.get_property("height").unwrap();
+    let receive_address_text = PropertyStr::wrap(&node, Role::App, "text", 0).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    let code = cc.compile("w - COPY_WIDTH").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE * 2.).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", MOCK_RECEIVE_ADDRESS).unwrap();
+    node.set_property_enum(atom, Role::App, "overflow_wrap", "anywhere").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    receive_layer.link(node);
+
+    // Copy button
+    let node = create_vector_art("receive_copy_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile("w - COPY_WIDTH / 2").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    let code = cc.compile(format!("{y} + (PADDING_Y * 2. + addr_height) / 2")).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_copy(COLOR_CYAN).scaled(COPY_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    receive_layer.link(node);
+
+    let node = create_button("receive_copy_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    let code = cc.compile("w").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    let code = cc.compile("PADDING_Y * 2 + addr_height").unwrap();
+    prop.set_expr(atom, Role::App, 3, code).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+    node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+
+    let (slot, recvr) = Slot::new("receive_copy_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let addr = receive_address_text.get();
+            miniquad::window::clipboard_set(&addr);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    receive_layer.link(node);
+
+    let sep = create_separator_expr(app, atom, &receive_layer, "receive_address_separator", &mut cc, &format!("{y} + PADDING_Y * 2 + addr_height + 1")).await;
+    let prop = sep.get_property("rect").unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+
+    receive_layer
+}
+
+async fn wallet_send_layer(
+    app: &App,
+    content: SceneNodePtr,
+    wallet_layer: SceneNodePtr,
+    i18n_fish: &I18nBabelFish,
+    window_scale: PropertyFloat32,
+) -> SceneNodePtr {
+    let mut cc = expr::Compiler::new();
+    cc.add_const_f32("HEADER_HEIGHT", HEADER_HEIGHT);
+    cc.add_const_f32("BUTTON_HEIGHT", BUTTON_HEIGHT);
+    cc.add_const_f32("PADDING_X", PADDING_X);
+    cc.add_const_f32("PADDING_Y", PADDING_Y);
+
+    let send_tx_data: Arc<Mutex<SendTxData>> = Arc::new(Mutex::new(SendTxData::new()));
+    let send_step1_layer =
+        wallet_send_layers(app, content.clone(), wallet_layer.clone(), i18n_fish, window_scale.clone(), send_tx_data).await;
+
+    send_step1_layer
+}
+
+/// Update positions for amount input wrapper and token symbol to center them together.
+fn update_amount_screen(
+    atom: &mut PropertyAtomicGuard,
+    amount_text: &str,
+    token_symbol: &str,
+    amount_wrapper_node: &SceneNodePtr,
+    amount_input_node: &SceneNodePtr,
+    token_node: &SceneNodePtr,
+    available_balance_node: Option<&SceneNodePtr>,
+) {
+    let mut cc = expr::Compiler::new();
+
+    let display_text = if amount_text.is_empty() { "0" } else { amount_text };
+    let char_count = display_text.chars().count() as f32;
+    let token_char_count = token_symbol.chars().count() as f32;
+
+    let text_width = char_count * AMOUNT_CHAR_WIDTH + 6.;
+    let token_width = token_char_count * AMOUNT_CHAR_WIDTH;
+    let total_width = text_width + AMOUNT_TOKEN_SPACING + token_width;
+    cc.add_const_f32("AMOUNT_TOTAL_WIDTH", total_width);
+    cc.add_const_f32("AMOUNT_WIDTH", text_width);
+    cc.add_const_f32("AMOUNT_TOKEN_SPACING", AMOUNT_TOKEN_SPACING);
+    cc.add_const_f32("AMOUNT_FONTSIZE", AMOUNT_FONTSIZE);
+
+    let wrapper_rect = amount_wrapper_node.get_property("rect").unwrap();
+    wrapper_rect.set_expr(atom, Role::App, 0, cc.compile("(w - AMOUNT_TOTAL_WIDTH) / 2").unwrap()).unwrap();
+
+    let width_code = cc.compile("AMOUNT_WIDTH").unwrap();
+    let amount_rect = amount_input_node.get_property("rect").unwrap();
+    amount_rect.set_expr(atom, Role::App, 2, width_code).unwrap();
+
+    // Reset scroll to prevent content from being cropped
+    if let Pimpl::Edit(edit) = amount_input_node.pimpl() {
+        edit.reset_scroll();
+    }
+
+    // Update token symbol rect (x position)
+    let token_rect = token_node.get_property("rect").unwrap();
+    token_rect.set_expr(atom, Role::App, 0, cc.compile("(w - AMOUNT_TOTAL_WIDTH) / 2 + AMOUNT_TOKEN_SPACING + AMOUNT_WIDTH").unwrap()).unwrap();
+
+    // Set available balance
+    if let Some(available_balance_node) = available_balance_node {
+        let available_balance = get_balance(token_symbol);
+        available_balance_node.set_property_str(atom, Role::App, "text", format!("{available_balance} available")).unwrap();
+    }
+}
+
+async fn wallet_send_layers(
+    app: &App,
+    content: SceneNodePtr,
+    wallet_layer: SceneNodePtr,
+    i18n_fish: &I18nBabelFish,
+    window_scale: PropertyFloat32,
+    send_tx_data: Arc<Mutex<SendTxData>>,
+) -> SceneNodePtr {
+    let atom = &mut PropertyAtomicGuard::none();
+
+    let mut cc = expr::Compiler::new();
+    cc.add_const_f32("HEADER_HEIGHT", HEADER_HEIGHT);
+    cc.add_const_f32("BUTTON_HEIGHT", BUTTON_HEIGHT);
+    cc.add_const_f32("BUTTON_FONTSIZE", BUTTON_FONTSIZE);
+    cc.add_const_f32("AMOUNT_FONTSIZE", AMOUNT_FONTSIZE);
+    cc.add_const_f32("BASE_FONTSIZE", BASE_FONTSIZE);
+    cc.add_const_f32("HINT_FONTSIZE", HINT_FONTSIZE);
+    cc.add_const_f32("PADDING_X", PADDING_X);
+    cc.add_const_f32("PADDING_Y", PADDING_Y);
+    cc.add_const_f32("RECIPIENT_INPUT_MARGIN", RECIPIENT_INPUT_MARGIN);
+    cc.add_const_f32("RECIPIENT_INPUT_PADDING_X", RECIPIENT_INPUT_PADDING_X);
+
+    // ============================================
+    // Step 1: Pick token layer
+    // ============================================
+    let send_step1_layer = create_layer("wallet_send_step1_layer");
+    let prop = send_step1_layer.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    send_step1_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    send_step1_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let send_step1_layer = send_step1_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(send_step1_layer.clone());
+    let step1_is_visible = PropertyBool::wrap(&send_step1_layer, Role::App, "is_visible", 0).unwrap();
+
+    create_bg_mesh(app, atom, &send_step1_layer, "send_bg").await;
+    create_header_bg(app, atom, &send_step1_layer, "send_header_bg").await;
+
+    // Back button
+    let node = create_vector_art("send_back_btn_bg");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    send_step1_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("send_back_btn");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+
+    let wallet_is_visible = PropertyBool::wrap(&wallet_layer, Role::App, "is_visible", 0).unwrap();
+    let renderer = app.renderer.clone();
+    let wallet_is_visible1 = wallet_is_visible.clone();
+    let step1_is_visible1 = step1_is_visible.clone();
+    let (slot, recvr) = Slot::new("send_back_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("send back button"));
+            wallet_is_visible1.set(atom, true);
+            step1_is_visible1.set(atom, false);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    send_step1_layer.link(node);
+
+    y += HEADER_HEIGHT;
+
+    create_title(app, atom, &send_step1_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+
+    create_subtitle(app, atom, &send_step1_layer, &window_scale, i18n_fish, "Pick a token to send", &mut y).await;
+
+    // ============================================
+    // Step 2: Recipient layer
+    // ============================================
+    let send_step2_layer = create_layer("wallet_send_step2_layer");
+    let prop = send_step2_layer.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    send_step2_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    send_step2_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let send_step2_layer = send_step2_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(send_step2_layer.clone());
+    let step2_is_visible = PropertyBool::wrap(&send_step2_layer, Role::App, "is_visible", 0).unwrap();
+
+    // ============================================
+    // Step 3: Amount layer
+    // ============================================
+    let send_step3_layer = create_layer("wallet_send_step3_layer");
+    let prop = send_step3_layer.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    send_step3_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    send_step3_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let send_step3_layer = send_step3_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(send_step3_layer.clone());
+    let step3_is_visible = PropertyBool::wrap(&send_step3_layer, Role::App, "is_visible", 0).unwrap();
+
+    // ============================================
+    // Step 4: Confirmation layer (amount as text, send button)
+    // ============================================
+    let send_step4_layer = create_layer("wallet_send_step4_layer");
+    let prop = send_step4_layer.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    send_step4_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    send_step4_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let send_step4_layer = send_step4_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(send_step4_layer.clone());
+    let step4_is_visible = PropertyBool::wrap(&send_step4_layer, Role::App, "is_visible", 0).unwrap();
+
+    // ============================================
+    // Step 5: Transaction in progress layer
+    // ============================================
+    let send_step5_layer = create_layer("wallet_send_step5_layer");
+    let prop = send_step5_layer.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    send_step5_layer.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
+    send_step5_layer.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let send_step5_layer = send_step5_layer.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    content.link(send_step5_layer.clone());
+    let step5_is_visible = PropertyBool::wrap(&send_step5_layer, Role::App, "is_visible", 0).unwrap();
+
+    // ========
+
+    let step2_is_visible2 = step2_is_visible.clone();
+    let send_tx_data2 = send_tx_data.clone();
+    let renderer = app.renderer.clone();
+    let step1_is_visible2 = step1_is_visible.clone();
+
+    // Subscribe to step2 visibility changes to call focus/unfocus when layer becomes visible/hidden
+    let step2_is_visible_for_focus = step2_is_visible.clone();
+    let sg_root_for_focus = app.sg_root.clone();
+    let renderer_for_focus = app.renderer.clone();
+    let step2_is_visible_sub = step2_is_visible.prop().subscribe_modify();
+    let listen_step2_visible = app.ex.spawn(async move {
+        while let Ok(_) = step2_is_visible_sub.receive().await {
+            let recipient_input_node = sg_root_for_focus.lookup_node("/window/content/wallet_send_step2_layer/send_recipient_input").unwrap();
+            if step2_is_visible_for_focus.get() {
+                // Focus when becoming visible
+                loop {
+                    msleep(16).await;
+                    let input_rect = PropertyRect::wrap(&recipient_input_node, Role::App, "rect").unwrap();
+                    if input_rect.has_cached() {
+                        break;
+                    }
+                }
+                recipient_input_node.call_method("focus", vec![]).await.unwrap();
+            } else {
+                // Unfocus when becoming hidden
+                recipient_input_node.call_method("unfocus", vec![]).await.unwrap();
+            }
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_step2_visible);
+
+    create_tokens_table(app, atom, &window_scale, i18n_fish, &send_step1_layer, MOCK_TOKENS, &mut y, move |sg_root, symbol, name, _balance| {
+        {
+            let mut data = send_tx_data2.lock().unwrap();
+            data.token_symbol = Some(symbol.to_string());
+            data.token_name = Some(name.to_string());
+        }
+
+        let mut atom = renderer.make_guard(gfxtag!("token selection"));
+        let selected_token_symbol = sg_root.lookup_node("/window/content/wallet_send_step2_layer/send_selected_token_symbol").unwrap();
+        selected_token_symbol.set_property_str(&mut atom, Role::App, "text", symbol).unwrap();
+        let selected_token_name = sg_root.lookup_node("/window/content/wallet_send_step2_layer/send_selected_token_name").unwrap();
+        selected_token_name.set_property_str(&mut atom, Role::App, "text", name).unwrap();
+
+        step1_is_visible2.set(&mut atom, false);
+        step2_is_visible2.set(&mut atom, true);
+    }).await;
+
+    create_bg_mesh(app, atom, &send_step2_layer, "send_bg2").await;
+    create_header_bg(app, atom, &send_step2_layer, "send_header_bg2").await;
+
+    // Back button
+    let node = create_vector_art("send_back_btn_bg2");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    send_step2_layer.link(node);
+
+    y = 0.;
+
+    let node = create_button("send_back_btn2");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+
+    let step1_is_visible2 = step1_is_visible.clone();
+    let step2_is_visible1 = step2_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let (slot, recvr) = Slot::new("send_back_clicked2");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("send step2 back button"));
+            step2_is_visible1.set(atom, false);
+            step1_is_visible2.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    send_step2_layer.link(node);
+
+    y += HEADER_HEIGHT;
+
+    create_title(app, atom, &send_step2_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+
+    // Selected token display
+    let node = create_text("send_selected_token_symbol");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "Token").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step2_layer.link(node);
+
+    let node = create_text("send_selected_token_name");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X + TOKEN_NAME_OFFSET).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "DRK").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let selected_token_text = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step2_layer.link(selected_token_text.clone());
+
+    y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
+
+    create_separator(app, atom, &send_step2_layer, "send_token_separator", &mut y).await;
+
+    // Recipient label
+    let node = create_text("send_recipient_label");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "Recipient").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step2_layer.link(node);
+
+    y += PADDING_Y * 2. + BASE_FONTSIZE;
+
+    // Recipient input outline
+    let node = create_vector_art("send_recipient_input_outline");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, RECIPIENT_INPUT_MARGIN).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    let code = cc.compile("w - RECIPIENT_INPUT_MARGIN * 2").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, RECIPIENT_INPUT_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        [0.2, 0.2745, 0.2784, 1.],
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    send_step2_layer.link(node);
+
+    // Recipient placeholder
+    let recipient_placeholder_node = create_text("send_recipient_input_placeholder");
+    let prop = recipient_placeholder_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, RECIPIENT_INPUT_MARGIN + RECIPIENT_INPUT_PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + (RECIPIENT_INPUT_HEIGHT - RECIPIENT_INPUT_FONTSIZE) / 2.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 200.).unwrap();
+    recipient_placeholder_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+    recipient_placeholder_node.set_property_f32(atom, Role::App, "font_size", RECIPIENT_INPUT_FONTSIZE).unwrap();
+    recipient_placeholder_node.set_property_str(atom, Role::App, "text", "recipient address...").unwrap();
+    let prop = recipient_placeholder_node.get_property("text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 0.45).unwrap();
+    recipient_placeholder_node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+
+    let recipient_placeholder_node = recipient_placeholder_node
+        .setup(|me| {
+            Text::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                i18n_fish.clone(),
+            )
+        })
+        .await;
+    send_step2_layer.link(recipient_placeholder_node.clone());
+
+    // Recipient input
+    let recipient_input = create_singleline_edit("send_recipient_input");
+    recipient_input.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    recipient_input.set_property_bool(atom, Role::App, "is_focused", false).unwrap();
+    let prop = recipient_input.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, RECIPIENT_INPUT_MARGIN + RECIPIENT_INPUT_PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    let code = cc.compile("parent_w - RECIPIENT_INPUT_MARGIN * 2 - RECIPIENT_INPUT_PADDING_X").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, RECIPIENT_INPUT_HEIGHT).unwrap();
+    recipient_input.set_property_f32(atom, Role::App, "font_size", RECIPIENT_INPUT_FONTSIZE).unwrap();
+    let prop = recipient_input.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    let prop = recipient_input.get_property("hi_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    let prop = recipient_input.get_property("text_hi_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.44).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.96).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = recipient_input.get_property("cursor_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.816).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.627).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    recipient_input.set_property_f32(atom, Role::App, "cursor_ascent", 0.).unwrap();
+    recipient_input.set_property_f32(atom, Role::App, "cursor_descent", RECIPIENT_INPUT_FONTSIZE*1.3).unwrap();
+    recipient_input.set_property_f32(atom, Role::App, "select_ascent", RECIPIENT_INPUT_FONTSIZE*1.3).unwrap();
+    recipient_input.set_property_f32(atom, Role::App, "select_descent", RECIPIENT_INPUT_FONTSIZE/3.).unwrap();
+    recipient_input.set_property_f32(atom, Role::App, "handle_descent", RECIPIENT_INPUT_FONTSIZE/2.5).unwrap();
+    recipient_input.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let recipient_input = recipient_input
+        .setup(|me| {
+            BaseEdit::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                BaseEditType::SingleLine,
+                app.ex.clone(),
+            )
+        })
+        .await;
+    send_step2_layer.link(recipient_input.clone());
+
+    y += RECIPIENT_INPUT_MARGIN + RECIPIENT_INPUT_HEIGHT;
+
+    // Add recipient button
+    let (node, btn_bg_valid, btn_bg_invalid, add_recipient_label_node) = create_bottom_button_with_states(
+        app,
+        atom,
+        &send_step2_layer,
+        "send_add_recipient_btn",
+        &mut cc,
+        "add recipient",
+        &window_scale,
+        i18n_fish,
+        false,
+    ).await;
+    let recipient_input2 = recipient_input.clone();
+    let recipient_text = recipient_input.get_property("text").unwrap();
+    let recipient_text_sub = recipient_text.subscribe_modify();
+    let renderer = app.renderer.clone();
+    let btn_bg_valid_clone = btn_bg_valid.clone();
+    let btn_bg_invalid_clone = btn_bg_invalid.clone();
+    let listen_recipient_text = app.ex.spawn(async move {
+        while let Ok(_) = recipient_text_sub.receive().await {
+            let atom = &mut renderer.make_guard(gfxtag!("wallet recipient input recv"));
+            let text_color = recipient_placeholder_node.get_property("text_color").unwrap();
+            let label_text_color = add_recipient_label_node.get_property("text_color").unwrap();
+            let btn_bg_valid_visible = btn_bg_valid_clone.get_property("is_visible").unwrap();
+            let btn_bg_invalid_visible = btn_bg_invalid_clone.get_property("is_visible").unwrap();
+            let addr = recipient_input2.get_property_str("text").unwrap();
+            if addr.is_empty() {
+                text_color.set_f32(atom, Role::App, 3, 0.45).unwrap();
+                // Grey color for invalid
+                label_text_color.set_f32(atom, Role::App, 0, 0.5).unwrap();
+                label_text_color.set_f32(atom, Role::App, 1, 0.5).unwrap();
+                label_text_color.set_f32(atom, Role::App, 2, 0.5).unwrap();
+                label_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
+                btn_bg_valid_visible.set_bool(atom, Role::App, 0, false).unwrap();
+                btn_bg_invalid_visible.set_bool(atom, Role::App, 0, true).unwrap();
+            } else {
+                text_color.set_f32(atom, Role::App, 3, 0.).unwrap();
+                // Cyan color for valid
+                if is_valid_address(&addr) {
+                    label_text_color.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+                    label_text_color.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+                    label_text_color.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+                    label_text_color.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+                    btn_bg_valid_visible.set_bool(atom, Role::App, 0, true).unwrap();
+                    btn_bg_invalid_visible.set_bool(atom, Role::App, 0, false).unwrap();
+                } else {
+                    // Grey color for invalid
+                    label_text_color.set_f32(atom, Role::App, 0, 0.5).unwrap();
+                    label_text_color.set_f32(atom, Role::App, 1, 0.5).unwrap();
+                    label_text_color.set_f32(atom, Role::App, 2, 0.5).unwrap();
+                    label_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
+                    btn_bg_valid_visible.set_bool(atom, Role::App, 0, false).unwrap();
+                    btn_bg_invalid_visible.set_bool(atom, Role::App, 0, true).unwrap();
+                }
+            }
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_recipient_text);
+
+    let step2_is_visible2 = step2_is_visible.clone();
+    let step3_is_visible2 = step3_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let recipient_input2 = recipient_input.clone();
+    let send_tx_data3 = send_tx_data.clone();
+    let sg_root = app.sg_root.clone();
+    let (slot, recvr) = Slot::new("send_add_recipient_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let text = recipient_input2.get_property_str("text").unwrap();
+            // Only proceed if address is valid
+            if !is_valid_address(&text) {
+                continue;
+            }
+
+            let atom = &mut renderer.make_guard(gfxtag!("add recipient button"));
+            {
+                let mut data = send_tx_data3.lock().unwrap();
+                data.recipient = Some(text.clone());
+            }
+            // Update step3 display with token and recipient
+            let data = send_tx_data3.lock().unwrap().clone();
+
+            if let Some(token_symbol) = &data.token_symbol {
+                let token_symbol_node = sg_root.lookup_node("/window/content/wallet_send_step3_layer/send_selected_token_symbol3").unwrap();
+                token_symbol_node.set_property_str(atom, Role::App, "text", token_symbol).unwrap();
+
+                // Update amount token symbol
+                if let Some(amount_token_node) = sg_root.lookup_node("/window/content/wallet_send_step3_layer/send_amount_token_symbol") {
+                    amount_token_node.set_property_str(atom, Role::App, "text", token_symbol).unwrap();
+
+                    // Update available balance
+                    let available_balance = get_balance(token_symbol);
+                    if let Some(available_balance_node) = sg_root.lookup_node("/window/content/wallet_send_step3_layer/send_available_balance") {
+                        available_balance_node.set_property_str(atom, Role::App, "text", format!("{available_balance} available")).unwrap();
+                    }
+                }
+            }
+            if let Some(token_name) = &data.token_name {
+                let token_name_node = sg_root.lookup_node("/window/content/wallet_send_step3_layer/send_selected_token_name3").unwrap();
+                token_name_node.set_property_str(atom, Role::App, "text", token_name).unwrap();
+            }
+            if let Some(recipient) = &data.recipient {
+                let recipient_node = sg_root.lookup_node("/window/content/wallet_send_step3_layer/send_recipient_value3").unwrap();
+                recipient_node.set_property_str(atom, Role::App, "text", recipient).unwrap();
+            }
+
+            step2_is_visible2.set(atom, false);
+            step3_is_visible2.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    // ============================================
+    // Step 3: Amount layer content
+    // ============================================
+    create_bg_mesh(app, atom, &send_step3_layer, "send_bg3").await;
+    create_header_bg(app, atom, &send_step3_layer, "send_header_bg3").await;
+
+    // Back button
+    let node = create_vector_art("send_back_btn_bg3");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    send_step3_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("send_back_btn3");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+
+    let step2_is_visible3 = step2_is_visible.clone();
+    let step3_is_visible1 = step3_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let (slot, recvr) = Slot::new("send_back_clicked3");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("send step3 back button"));
+            step3_is_visible1.set(atom, false);
+            step2_is_visible3.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    send_step3_layer.link(node);
+
+    y += HEADER_HEIGHT;
+
+    create_title(app, atom, &send_step3_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+
+    // Selected token display (same layout as step2)
+    let node = create_text("send_selected_token_symbol3");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    // Set initial token value from send_tx_data
+    let token_symbol_text = send_tx_data.lock().unwrap().token_symbol.clone().unwrap_or_else(|| "".to_string());
+    node.set_property_str(atom, Role::App, "text", &token_symbol_text).unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step3_layer.link(node);
+
+    let node = create_text("send_selected_token_name3");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X + TOKEN_NAME_OFFSET).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    // Set initial token value from send_tx_data
+    let token_name_text = send_tx_data.lock().unwrap().token_name.clone().unwrap_or_else(|| "---".to_string());
+    node.set_property_str(atom, Role::App, "text", &token_name_text).unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let selected_token_text3 = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step3_layer.link(selected_token_text3.clone());
+
+    y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
+
+    create_separator(app, atom, &send_step3_layer, "send_token_separator3", &mut y).await;
+
+    // Recipient display
+    let node = create_text("send_recipient_label3");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "Recipient").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step3_layer.link(node);
+
+    y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
+
+    // Recipient address value
+    let node = create_text("send_recipient_value3");
+    let addr_h_prop = node.get_property("height").unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    let code = cc.compile("w - PADDING_X * 2").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "").unwrap();
+    node.set_property_enum(atom, Role::App, "overflow_wrap", "anywhere").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step3_layer.link(node);
+
+    // Separator line
+    let mut y_ = y.clone();
+    let y2 = format!("{y} + (PADDING_Y * 2. + addr_height) + 1");
+    let node = create_separator(app, atom, &send_step3_layer, "send_amount_label_separator", &mut y_).await;
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile(&y2).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+
+    // Available balance text
+    let available_balance_node = create_text("send_available_balance");
+    let prop = available_balance_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X * 2 - BUTTON_HEIGHT - PADDING_Y - BASE_FONTSIZE - 1").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    available_balance_node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    available_balance_node.set_property_str(atom, Role::App, "text", "").unwrap();
+    let prop = available_balance_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    available_balance_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let available_balance_node = available_balance_node.setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())).await;
+    send_step3_layer.link(available_balance_node.clone());
+
+    // Separator line below available balance
+    create_separator_expr(
+        app,
+        atom,
+        &send_step3_layer,
+        "available_balance_separator",
+        &mut cc,
+        "h - PADDING_X * 2 - BUTTON_HEIGHT",
+    ).await;
+
+    // Amount wrapper layer
+    let amount_y = format!("({y2}) + (h - ({y2}) - BUTTON_HEIGHT - PADDING_X * 2 - PADDING_Y - BASE_FONTSIZE - AMOUNT_FONTSIZE - 1) / 2");
+    let amount_wrapper = create_layer("send_amount_wrapper");
+    amount_wrapper.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    let prop = amount_wrapper.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    let code = cc.compile(&amount_y).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+    amount_wrapper.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let amount_wrapper = amount_wrapper.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    send_step3_layer.link(amount_wrapper.clone());
+
+    // Amount input
+    let input_node = create_singleline_edit("send_amount_input");
+    input_node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = input_node.get_property("rect").unwrap();
+    // Position at (0, 0) within wrapper - wrapper controls the position
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    cc.add_const_f32("AMOUNT_CHAR_WIDTH", AMOUNT_CHAR_WIDTH+6.); // Initial width for "0"
+    let code = cc.compile("AMOUNT_CHAR_WIDTH").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
+    input_node.set_property_f32(atom, Role::App, "font_size", AMOUNT_FONTSIZE).unwrap();
+    let prop = input_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    let prop = input_node.get_property("hi_bg_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::PaperLight {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    let prop = input_node.get_property("text_hi_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.44).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.96).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    let prop = input_node.get_property("cursor_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.816).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.627).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    input_node.set_property_f32(atom, Role::App, "cursor_ascent", 0.).unwrap();
+    input_node.set_property_f32(atom, Role::App, "cursor_descent", AMOUNT_FONTSIZE*1.3).unwrap();
+    input_node.set_property_f32(atom, Role::App, "select_ascent", AMOUNT_FONTSIZE*1.3).unwrap();
+    input_node.set_property_f32(atom, Role::App, "select_descent", AMOUNT_FONTSIZE/3.).unwrap();
+    input_node.set_property_f32(atom, Role::App, "handle_descent", AMOUNT_FONTSIZE/2.5).unwrap();
+    input_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let input_node = input_node
+        .setup(|me| {
+            BaseEdit::new(
+                me,
+                window_scale.clone(),
+                app.renderer.clone(),
+                BaseEditType::SingleLine,
+                app.ex.clone(),
+            )
+        })
+        .await;
+    amount_wrapper.link(input_node.clone());
+
+    // Set initial text to "0" for the amount input
+    let input_text_prop = input_node.get_property("text").unwrap();
+    input_text_prop.set_str(atom, Role::App, 0, "0").unwrap();
+    // Update editor's internal state
+    if let Pimpl::Edit(edit) = input_node.pimpl() {
+        edit.on_text_prop_changed();
+    }
+
+    // Token symbol text node (displayed next to amount)
+    let token_symbol_node = create_text("send_amount_token_symbol");
+    let prop = token_symbol_node.get_property("rect").unwrap();
+    // Position will be dynamically updated based on amount text
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    let code = cc.compile(format!("{amount_y} - AMOUNT_FONTSIZE * 0.1")).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+    token_symbol_node.set_property_f32(atom, Role::App, "font_size", AMOUNT_FONTSIZE).unwrap();
+    // Initial text - will be set from send_tx_data.token_symbol
+    let token_symbol_initial = send_tx_data.lock().unwrap().token_symbol.clone().unwrap_or_else(|| "".to_string());
+    token_symbol_node.set_property_str(atom, Role::App, "text", &token_symbol_initial).unwrap();
+    let prop = token_symbol_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    token_symbol_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let token_symbol_node = token_symbol_node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step3_layer.link(token_symbol_node.clone());
+
+    let token_symbol_initial = send_tx_data.lock().unwrap().token_symbol.clone().unwrap_or_else(|| "".to_string());
+    update_amount_screen(
+        atom,
+        "0",
+        &token_symbol_initial,
+        &amount_wrapper,
+        &input_node,
+        &token_symbol_node,
+        Some(&available_balance_node),
+    );
+
+    y += PADDING_Y * 2. + BUTTON_HEIGHT + 10.;
+
+    // Add amount button with states
+    let (node, btn_bg_valid, btn_bg_invalid, add_amount_label_node) = create_bottom_button_with_states(
+        app,
+        atom,
+        &send_step3_layer,
+        "send_add_amount_btn",
+        &mut cc,
+        "add amount",
+        &window_scale,
+        i18n_fish,
+        false, // Start with invalid state (grey)
+    ).await;
+
+    // Amount input validation listener
+    let amount_input2 = input_node.clone();
+    let amount_wrapper2 = amount_wrapper.clone();
+    let available_balance_node2 = available_balance_node.clone();
+    let token_symbol_node2 = token_symbol_node.clone();
+    let send_tx_data5 = send_tx_data.clone();
+    let amount_text = amount_input2.get_property("text").unwrap();
+    let amount_text_sub = amount_text.subscribe_modify();
+    let renderer = app.renderer.clone();
+    let btn_bg_valid_clone = btn_bg_valid.clone();
+    let btn_bg_invalid_clone = btn_bg_invalid.clone();
+    let listen_amount_text = app.ex.spawn(async move {
+        let mut old_amount_text = "0".to_string();
+        while let Ok(_) = amount_text_sub.receive().await {
+            let atom = &mut renderer.make_guard(gfxtag!("wallet amount input recv"));
+            let label_text_color = add_amount_label_node.get_property("text_color").unwrap();
+            let btn_bg_valid_visible = btn_bg_valid_clone.get_property("is_visible").unwrap();
+            let btn_bg_invalid_visible = btn_bg_invalid_clone.get_property("is_visible").unwrap();
+            let amount_input_text_color = amount_input2.get_property("text_color").unwrap();
+            let amount = amount_input2.get_property_str("text").unwrap();
+
+            let sanitized_amount = if amount.is_empty() {
+                "0".to_string()
+            } else {
+                let mut result = String::new();
+                let mut dot_seen = false;
+                for c in amount.chars() {
+                    if c.is_ascii_digit() {
+                        result.push(c);
+                    } else if c == '.' && !dot_seen {
+                        dot_seen = true;
+                        result.push(c);
+                    }
+                }
+
+                // Strip leading zeros
+                let parts: Vec<&str> = result.split('.').collect();
+                let integer_part = if !parts.is_empty() && !parts[0].is_empty() {
+                    let trimmed = parts[0].trim_start_matches('0');
+                    let trimmed = if trimmed.is_empty() {
+                        "0".to_string()
+                    } else {
+                        trimmed.to_string()
+                    };
+
+                    // Forces removing "0" chars when the amount was previously "0".
+                    // This is so that when the input cursor is BEFORE the initial
+                    // "0" and you insert a digit, it removes the initial "0".
+                    if old_amount_text == "0" && trimmed != "0" && parts.len() == 1 {
+                        trimmed.chars().filter(|&c| c != '0').collect()
+                    } else {
+                        trimmed
+                    }
+                } else {
+                    "0".to_string()
+                };
+
+                if parts.len() > 1 {
+                    format!("{}.{}", integer_part, parts[1])
+                } else {
+                    integer_part
+                }
+            };
+
+            if sanitized_amount != amount {
+                let input_text_prop = amount_input2.get_property("text").unwrap();
+                input_text_prop.set_str(atom, Role::Ignored, 0, &sanitized_amount).unwrap();
+                if let Pimpl::Edit(edit) = amount_input2.pimpl() {
+                    edit.on_text_prop_changed();
+                }
+            }
+
+            old_amount_text = sanitized_amount.clone();
+
+            // Update positions to center amount and token symbol
+            let token_symbol = send_tx_data5.lock().unwrap().token_symbol.clone().unwrap_or_else(|| "".to_string());
+            update_amount_screen(
+                atom,
+                &sanitized_amount,
+                &token_symbol,
+                &amount_wrapper2,
+                &amount_input2,
+                &token_symbol_node2,
+                Some(&available_balance_node2),
+            );
+            let available_balance = get_balance(&token_symbol);
+            let is_valid = if sanitized_amount == "0" {
+                false
+            } else {
+                match sanitized_amount.parse::<f32>() {
+                    Ok(v) if v > 0. && v <= available_balance => true,
+                    _ => false,
+                }
+            };
+
+            // Set amount input text color: grey if "0", white otherwise
+            if sanitized_amount == "0" {
+                amount_input_text_color.set_f32(atom, Role::App, 0, 0.5).unwrap();
+                amount_input_text_color.set_f32(atom, Role::App, 1, 0.5).unwrap();
+                amount_input_text_color.set_f32(atom, Role::App, 2, 0.5).unwrap();
+                amount_input_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
+            } else {
+                amount_input_text_color.set_f32(atom, Role::App, 0, 1.).unwrap();
+                amount_input_text_color.set_f32(atom, Role::App, 1, 1.).unwrap();
+                amount_input_text_color.set_f32(atom, Role::App, 2, 1.).unwrap();
+                amount_input_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
+            }
+
+            if is_valid {
+                label_text_color.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+                label_text_color.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+                label_text_color.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+                label_text_color.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+                btn_bg_valid_visible.set_bool(atom, Role::App, 0, true).unwrap();
+                btn_bg_invalid_visible.set_bool(atom, Role::App, 0, false).unwrap();
+            } else {
+                label_text_color.set_f32(atom, Role::App, 0, 0.5).unwrap();
+                label_text_color.set_f32(atom, Role::App, 1, 0.5).unwrap();
+                label_text_color.set_f32(atom, Role::App, 2, 0.5).unwrap();
+                label_text_color.set_f32(atom, Role::App, 3, 1.).unwrap();
+                btn_bg_valid_visible.set_bool(atom, Role::App, 0, false).unwrap();
+                btn_bg_invalid_visible.set_bool(atom, Role::App, 0, true).unwrap();
+            }
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_amount_text);
+
+    let renderer = app.renderer.clone();
+    let amount_input2 = input_node.clone();
+    let send_tx_data4 = send_tx_data.clone();
+    let step3_is_visible2 = step3_is_visible.clone();
+    let step4_is_visible2 = step4_is_visible.clone();
+    let sg_root = app.sg_root.clone();
+    let (slot, recvr) = Slot::new("send_add_amount_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let text = amount_input2.get_property_str("text").unwrap();
+            let available_balance = 100.24_f32; // TODO
+            let is_valid = if text.is_empty() {
+                false
+            } else {
+                match text.parse::<f32>() {
+                    Ok(v) if v > 0. && v <= available_balance => true,
+                    _ => false,
+                }
+            };
+            if is_valid {
+                let atom = &mut renderer.make_guard(gfxtag!("add amount button"));
+                {
+                    let mut data = send_tx_data4.lock().unwrap();
+                    data.amount = Some(text.clone());
+                }
+                // Update step4 display with transaction data
+                let data = send_tx_data4.lock().unwrap().clone();
+                if let Some(token_symbol) = &data.token_symbol {
+                    let token_symbol_node = sg_root.lookup_node("/window/content/wallet_send_step4_layer/send_selected_token_symbol4").unwrap();
+                    token_symbol_node.set_property_str(atom, Role::App, "text", token_symbol).unwrap();
+
+                    if let Some(amount_token_node) = sg_root.lookup_node("/window/content/wallet_send_step4_layer/send_amount_token_symbol4") {
+                        amount_token_node.set_property_str(atom, Role::App, "text", token_symbol).unwrap();
+                    }
+                }
+                if let Some(token_name) = &data.token_name {
+                    let token_name_node = sg_root.lookup_node("/window/content/wallet_send_step4_layer/send_selected_token_name4").unwrap();
+                    token_name_node.set_property_str(atom, Role::App, "text", token_name).unwrap();
+                }
+                if let Some(recipient) = &data.recipient {
+                    let recipient_node = sg_root.lookup_node("/window/content/wallet_send_step4_layer/send_recipient_value4").unwrap();
+                    recipient_node.set_property_str(atom, Role::App, "text", recipient).unwrap();
+                }
+                if let Some(amount) = &data.amount {
+                    let amount_node = sg_root.lookup_node("/window/content/wallet_send_step4_layer/send_amount_wrapper4/send_amount_text4").unwrap();
+                    amount_node.set_property_str(atom, Role::App, "text", amount).unwrap();
+                }
+                step3_is_visible2.set(atom, false);
+                step4_is_visible2.set(atom, true);
+            }
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    // Add listener for step3 visibility to focus/unfocus amount input
+    let step3_is_visible_clone = step3_is_visible.clone();
+    let renderer_clone = app.renderer.clone();
+    let amount_wrapper_clone = amount_wrapper.clone();
+    let input_node_clone = input_node.clone();
+    let token_symbol_node_clone = token_symbol_node.clone();
+    let send_tx_data_clone = send_tx_data.clone();
+    let step3_is_visible_sub = step3_is_visible.prop().subscribe_modify();
+    let listen_step3_visible = app.ex.spawn(async move {
+        while let Ok(_) = step3_is_visible_sub.receive().await {
+            if step3_is_visible_clone.get() {
+                loop { // TODO: this waits for draw, there's probably a better way
+                    msleep(50).await;
+                    let input_rect = PropertyRect::wrap(&input_node_clone, Role::App, "rect").unwrap();
+                    if input_rect.has_cached() {
+                        break;
+                    }
+                }
+
+                // Focus the amount input
+                input_node_clone.call_method("focus", vec![]).await.unwrap();
+
+                // Get the current token symbol and update positions
+                let token_symbol = send_tx_data_clone.lock().unwrap().token_symbol.clone().unwrap_or_default();
+                if !token_symbol.is_empty() {
+                    let atom = &mut renderer_clone.make_guard(gfxtag!("update amount positions on visible"));
+                    let current_amount = input_node_clone.get_property_str("text").unwrap();
+                    update_amount_screen(
+                        atom,
+                        &current_amount,
+                        &token_symbol,
+                        &amount_wrapper_clone,
+                        &input_node_clone,
+                        &token_symbol_node_clone,
+                        Some(&available_balance_node),
+                    );
+                }
+            } else {
+                // Unfocus when becoming hidden
+                input_node_clone.call_method("unfocus", vec![]).await.unwrap();
+            }
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_step3_visible);
+
+    // ============================================
+    // Step 4: Confirmation layer content
+    // ============================================
+    create_bg_mesh(app, atom, &send_step4_layer, "send_bg4").await;
+    create_header_bg(app, atom, &send_step4_layer, "send_header_bg4").await;
+
+    // Back button
+    let node = create_vector_art("send_back_btn_bg4");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, BACKARROW_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, BACKARROW_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 500.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 500.).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let shape = shape::create_back_arrow().scaled(BACKARROW_SCALE);
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    send_step4_layer.link(node);
+
+    let mut y = 0.;
+
+    let node = create_button("send_back_btn4");
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, y).unwrap();
+    prop.set_f32(atom, Role::App, 2, WALLET_BTN_SIZE * 2.).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+
+    let step3_is_visible2 = step3_is_visible.clone();
+    let step4_is_visible1 = step4_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let (slot, recvr) = Slot::new("send_back_clicked4");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("send step4 back button"));
+            step4_is_visible1.set(atom, false);
+            step3_is_visible2.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    send_step4_layer.link(node);
+
+    y += HEADER_HEIGHT;
+
+    create_title(app, atom, &send_step4_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+
+    // Selected token display
+    let node = create_text("send_selected_token_symbol4");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "Token").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step4_layer.link(node);
+
+    let node = create_text("send_selected_token_name4");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X + TOKEN_NAME_OFFSET).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "DRK").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step4_layer.link(node);
+
+    y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
+
+    create_separator(app, atom, &send_step4_layer, "send_token_separator4", &mut y).await;
+
+    // Recipient label
+    let node = create_text("send_recipient_label4");
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "Recipient").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step4_layer.link(node);
+
+    y += PADDING_Y * 2. + BASE_FONTSIZE + 1.;
+
+    // Recipient address value
+    let recipient_addr_node = create_text("send_recipient_value4");
+    let addr_h_prop = recipient_addr_node.get_property("height").unwrap();
+    let prop = recipient_addr_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    let code = cc.compile("w - PADDING_X * 2").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
+    recipient_addr_node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
+    recipient_addr_node.set_property_str(atom, Role::App, "text", "").unwrap();
+    recipient_addr_node.set_property_enum(atom, Role::App, "overflow_wrap", "anywhere").unwrap();
+    let prop = recipient_addr_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    recipient_addr_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let recipient_addr_node = recipient_addr_node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step4_layer.link(recipient_addr_node.clone());
+
+    // Separator line
+    let mut y_ = y.clone();
+    let y2 = format!("{y} + (PADDING_Y * 2. + addr_height) + 1");
+    let node = create_separator(app, atom, &send_step4_layer, "send_amount_label_separator4", &mut y_).await;
+    let prop = node.get_property("rect").unwrap();
+    let code = cc.compile(&y2).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+
+    // Amount display - same layout as step3's amount input
+    let amount_y = format!("({y2}) + (h - ({y2}) - BUTTON_HEIGHT - PADDING_X * 2 - PADDING_Y - BASE_FONTSIZE - AMOUNT_FONTSIZE - 1) / 2 - AMOUNT_FONTSIZE * 0.1");
+    let amount_wrapper = create_layer("send_amount_wrapper4");
+    amount_wrapper.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    let prop = amount_wrapper.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    let code = cc.compile(&amount_y).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+    amount_wrapper.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let amount_wrapper = amount_wrapper.setup(|me| Layer::new(me, app.renderer.clone())).await;
+    send_step4_layer.link(amount_wrapper.clone());
+
+    // Amount text
+    let amount_text_node = create_text("send_amount_text4");
+    let prop = amount_text_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
+    amount_text_node.set_property_f32(atom, Role::App, "font_size", AMOUNT_FONTSIZE).unwrap();
+    let prop = amount_text_node.get_property("text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    amount_text_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    amount_text_node.set_property_str(atom, Role::App, "text", "0").unwrap();
+    let amount_text_node = amount_text_node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    amount_wrapper.link(amount_text_node.clone());
+
+    // Token symbol text node (displayed next to amount)
+    let token_symbol_node = create_text("send_amount_token_symbol4");
+    let prop = token_symbol_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    let code = cc.compile(amount_y).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, AMOUNT_FONTSIZE).unwrap();
+    prop.add_depend(&addr_h_prop, 0, "addr_height");
+    token_symbol_node.set_property_f32(atom, Role::App, "font_size", AMOUNT_FONTSIZE).unwrap();
+    token_symbol_node.set_property_str(atom, Role::App, "text", "").unwrap();
+    let prop = token_symbol_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    token_symbol_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let token_symbol_node = token_symbol_node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step4_layer.link(token_symbol_node.clone());
+
+    // Transaction fee label
+    let tx_fee_label_node = create_text("send_fee_label");
+    let prop = tx_fee_label_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X * 2 - BUTTON_HEIGHT - PADDING_Y - BASE_FONTSIZE - 1").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    tx_fee_label_node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    tx_fee_label_node.set_property_str(atom, Role::App, "text", "Transaction fee").unwrap();
+    let prop = tx_fee_label_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    tx_fee_label_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let tx_fee_label_node = tx_fee_label_node.setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())).await;
+    send_step4_layer.link(tx_fee_label_node.clone());
+
+    // Transaction fee value
+    let tx_fee_value_node = create_text("send_fee_value");
+    let prop = tx_fee_value_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X * 2 - BUTTON_HEIGHT - PADDING_Y - BASE_FONTSIZE - 1").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    tx_fee_value_node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    tx_fee_value_node.set_property_str(atom, Role::App, "text", MOCK_TX_FEE).unwrap();
+    tx_fee_value_node.set_property_enum(atom, Role::App, "text_align", "end").unwrap();
+    let prop = tx_fee_value_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    tx_fee_value_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let tx_fee_value_node = tx_fee_value_node.setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())).await;
+    send_step4_layer.link(tx_fee_value_node.clone());
+
+    // Separator line
+    create_separator_expr(
+        app,
+        atom,
+        &send_step4_layer,
+        "tx_fee_separator",
+        &mut cc,
+        "h - PADDING_X * 2 - BUTTON_HEIGHT",
+    ).await;
+
+    y += PADDING_Y * 2. + BUTTON_HEIGHT + 10.;
+
+    // Send button (bottom button)
+    let node = create_bottom_button(
+        app,
+        atom,
+        &send_step4_layer,
+        "send_send_btn",
+        &mut cc,
+        Some("send"),
+        &window_scale,
+        i18n_fish,
+    ).await;
+
+    let renderer = app.renderer.clone();
+    let sg_root = app.sg_root.clone();
+    let step4_is_visible1 = step4_is_visible.clone();
+    let step5_is_visible1 = step5_is_visible.clone();
+    let send_tx_data_clone = send_tx_data.clone();
+    let (slot, recvr) = Slot::new("send_send_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("send button"));
+
+            // Update step5 with transaction info
+            let data = send_tx_data_clone.lock().unwrap().clone();
+
+            if let Some(tx_info_node) = sg_root.lookup_node("/window/content/wallet_send_step5_layer/send_tx_info5") {
+                let amount = data.amount.as_deref().unwrap_or("0");
+                let token_symbol = data.token_symbol.as_deref().unwrap_or("DRK");
+                let recipient = data.recipient.as_deref().unwrap_or("recipient");
+                let tx_text = format!("Sending {} {} to {}", amount, token_symbol, recipient);
+                tx_info_node.set_property_str(atom, Role::App, "text", tx_text).unwrap();
+            }
+
+            step4_is_visible1.set(atom, false);
+            step5_is_visible1.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    // Add listener for step4 visibility to update amount positions
+    let step4_is_visible_clone = step4_is_visible.clone();
+    let renderer_clone = app.renderer.clone();
+    let amount_wrapper_clone = amount_wrapper.clone();
+    let amount_text_node_clone = amount_text_node.clone();
+    let token_symbol_node_clone = token_symbol_node.clone();
+    let send_tx_data_clone2 = send_tx_data.clone();
+    let step4_is_visible_sub = step4_is_visible.prop().subscribe_modify();
+    let listen_step4_visible = app.ex.spawn(async move {
+        while let Ok(_) = step4_is_visible_sub.receive().await {
+            if step4_is_visible_clone.get() {
+                loop {
+                    msleep(50).await;
+                    let text_rect = PropertyRect::wrap(&amount_text_node_clone, Role::App, "rect").unwrap();
+                    if text_rect.has_cached() {
+                        break;
+                    }
+                }
+
+                let atom = &mut renderer_clone.make_guard(gfxtag!("update step4 amount positions"));
+                let data = send_tx_data_clone2.lock().unwrap().clone();
+
+                let amount_text = data.amount.unwrap_or_else(|| "0".to_string());
+                let token_symbol = data.token_symbol.unwrap_or_else(|| "".to_string());
+
+                // Update positions to center amount and token symbol
+                update_amount_screen(
+                    atom,
+                    &amount_text,
+                    &token_symbol,
+                    &amount_wrapper_clone,
+                    &amount_text_node_clone,
+                    &token_symbol_node_clone,
+                    None,
+                );
+            }
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_step4_visible);
+
+    // ============================================
+    // Step 5: Transaction in progress layer content
+    // ============================================
+    create_bg_mesh(app, atom, &send_step5_layer, "send_bg5").await;
+    create_header_bg(app, atom, &send_step5_layer, "send_header_bg5").await;
+
+    y = 0.;
+    y += HEADER_HEIGHT;
+
+    create_title(app, atom, &send_step5_layer, &window_scale, i18n_fish, "SEND", &mut y).await;
+
+    // Subtitle: "Transaction in progress..."
+    create_subtitle(app, atom, &send_step5_layer, &window_scale, i18n_fish, "Transaction in progress...", &mut y).await;
+
+    // Transaction info text: "Sending {amount} {token_symbol} to {recipient_address}"
+    let node = create_text("send_tx_info5");
+    let info_h_prop = node.get_property("height").unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, y + PADDING_Y).unwrap();
+    let code = cc.compile("w - PADDING_X * 2").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", "Sending 0 DRK to recipient").unwrap();
+    node.set_property_enum(atom, Role::App, "overflow_wrap", "anywhere").unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step5_layer.link(node);
+
+    let sep = create_separator(app, atom, &send_step5_layer, "send_info_separator5", &mut 0.).await;
+    let prop = sep.get_property("rect").unwrap();
+    let code = cc.compile(format!("{y} + PADDING_Y * 2 + info_height + 1")).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.add_depend(&info_h_prop, 0, "info_height");
+
+    // Hint text
+    let hint_text_node = create_text("send_close_hint5");
+    let prop = hint_text_node.get_property("rect").unwrap();
+    let code = cc.compile("w / 2 - (HINT_FONTSIZE * 0.7 * 31) / 2").unwrap();
+    prop.set_expr(atom, Role::App, 0, code).unwrap();
+    let code = cc.compile("h - PADDING_X * 2 - BUTTON_HEIGHT - PADDING_Y - HINT_FONTSIZE * 2").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("HINT_FONTSIZE * 0.7 * 31").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, HINT_FONTSIZE/2.).unwrap();
+    hint_text_node.set_property_f32(atom, Role::App, "font_size", HINT_FONTSIZE).unwrap();
+    hint_text_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
+    hint_text_node.set_property_str(atom, Role::App, "text", "You can close this screen while the transaction is confirming.").unwrap();
+    let prop = hint_text_node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.7).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 0.7).unwrap();
+    }
+    hint_text_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+
+    let node = hint_text_node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    send_step5_layer.link(node);
+
+    // Close label (bottom button)
+    let close_label_node = create_text("send_close_label");
+    let prop = close_label_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2.").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    close_label_node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
+    close_label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
+    close_label_node.set_property_str(atom, Role::App, "text", "close").unwrap();
+    let prop = close_label_node.get_property("text_color").unwrap();
+    prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+    prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+    prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+    prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+    close_label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+    let close_label_node = close_label_node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+
+    // Close button
+    let node = create_bottom_button(
+        app,
+        atom,
+        &send_step5_layer,
+        "send_close_btn",
+        &mut cc,
+        Some("close"),
+        &window_scale,
+        i18n_fish,
+    ).await;
+
+    // Click handler
+    let wallet_is_visible = wallet_is_visible.clone();
+    let step5_is_visible = step5_is_visible.clone();
+    let renderer = app.renderer.clone();
+    let (slot, recvr) = Slot::new("send_close_clicked");
+    node.register("click", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(_) = recvr.recv().await {
+            let atom = &mut renderer.make_guard(gfxtag!("close button"));
+            step5_is_visible.set(atom, false);
+            wallet_is_visible.set(atom, true);
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
+    send_step1_layer
+}
+
+/// Creates a title text node with separator line.
+/// Returns the text node after setup, linked to the layer
+async fn create_title(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    window_scale: &PropertyFloat32,
+    i18n_fish: &I18nBabelFish,
+    name: &str,
+    y: &mut f32,
+) -> SceneNodePtr {
+    let node = create_text(name);
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, *y + TITLE_PADDING).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", name).unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())).await;
+    layer.link(node.clone());
+
+    *y += TITLE_PADDING * 2. + TITLE_FONTSIZE + 1.;
+
+    create_separator(app, atom, layer, &format!("{}_separator", name), y).await;
+    node
+}
+
+/// Creates a subtitle text node with separator line (e.g., "Pick a token to send", "Recipient", "Address")
+/// Returns the text node after setup, linked to the layer
+async fn create_subtitle(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    window_scale: &PropertyFloat32,
+    i18n_fish: &I18nBabelFish,
+    text: &str,
+    y: &mut f32,
+) -> SceneNodePtr {
+    let node = create_text(text);
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    prop.set_f32(atom, Role::App, 1, *y + PADDING_Y).unwrap();
+    prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+    prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
+    node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
+    node.set_property_str(atom, Role::App, "text", text).unwrap();
+    let prop = node.get_property("text_color").unwrap();
+    if COLOR_SCHEME == ColorScheme::DarkMode {
+        prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let node = node.setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())).await;
+    layer.link(node.clone());
+
+    *y += PADDING_Y * 2. + TITLE_FONTSIZE + 1.;
+
+    create_separator(app, atom, layer, &format!("{}_separator", text), y).await;
+
+    node
+}
+
+/// Creates a background mesh with gradient box.
+async fn create_bg_mesh(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    name: &str,
+) {
+    let node = create_vector_art(name);
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_gradient_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [[0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.8]],
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    layer.link(node);
+}
+
+/// Creates a header background with filled box and separator line at bottom.
+async fn create_header_bg(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    name: &str,
+) {
+    let node = create_vector_art(name);
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
+
+    let (bg_color, sep_color) = match COLOR_SCHEME {
+        ColorScheme::DarkMode => ([0., 0.11, 0.11, 1.], [0.2, 0.2745, 0.2784, 1.]),
+        ColorScheme::PaperLight => ([1., 1., 1., 1.], [0., 0.6, 0.65, 1.]),
+    };
+
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::const_f32(HEADER_HEIGHT),
+        bg_color,
+    );
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(HEADER_HEIGHT - 1.),
+        expr::load_var("w"),
+        expr::const_f32(HEADER_HEIGHT),
+        sep_color,
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    layer.link(node);
+}
+
+/// Creates a separator line at the given y expression.
+/// Returns the separator node after setup, linked to the layer
+async fn create_separator_expr(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    name: &str,
+    cc: &mut expr::Compiler,
+    y_expr: &str,
+) -> SceneNodePtr {
+    let sep_node = create_vector_art(name);
+    let prop = sep_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    let code = cc.compile(y_expr).unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    sep_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::const_f32(1.),
+        [0.2, 0.2745, 0.2784, 1.],
+    );
+    let sep_node = sep_node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    layer.link(sep_node.clone());
+    sep_node
+}
+
+/// Creates a separator line at the current y position and increments y.
+/// Returns the separator node after setup, linked to the layer
+async fn create_separator(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    name: &str,
+    y: &mut f32,
+) -> SceneNodePtr {
+    let sep_node = create_vector_art(name);
+    let prop = sep_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 1, *y).unwrap();
+    prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
+    prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    sep_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::const_f32(1.),
+        [0.2, 0.2745, 0.2784, 1.],
+    );
+    let sep_node = sep_node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    layer.link(sep_node.clone());
+
+    *y += 1.;
+    sep_node
+}
+
+async fn create_tokens_table(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    window_scale: &PropertyFloat32,
+    i18n_fish: &I18nBabelFish,
+    layer: &SceneNodePtr,
+    tokens: &[(&str, &str, f32)],
+    y: &mut f32,
+    on_click: impl Fn(&SceneNodePtr, &str, &str, &f32) + Clone + Send + Sync + 'static,
+) {
+    use crate::prop::Role;
+    use crate::scene::Slot;
+
+    let mut cc = expr::Compiler::new();
+    cc.add_const_f32("PADDING_X", PADDING_X);
+
+    for (i, (token, full_name, balance)) in tokens.iter().enumerate() {
+        let row_y = *y;
+        let row_height = PADDING_Y * 2. + BASE_FONTSIZE + 1.;
+
+        // Token row button
+        let node = create_button(&format!("token_row_btn_{}", i));
+        node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+        let prop = node.get_property("rect").unwrap();
+        prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+        prop.set_f32(atom, Role::App, 1, row_y).unwrap();
+        let code = cc.compile("w").unwrap();
+        prop.set_expr(atom, Role::App, 2, code).unwrap();
+        prop.set_f32(atom, Role::App, 3, row_height).unwrap();
+
+        let callback = on_click.clone();
+        let token_str = token.to_string();
+        let name_str = full_name.to_string();
+        let balance2 = *balance;
+        let sg_root = app.sg_root.clone();
+        let (slot, recvr) = Slot::new(&format!("token_row_clicked_{}", i));
+        node.register("click", slot).unwrap();
+        let listen_click = app.ex.spawn(async move {
+            while let Ok(_) = recvr.recv().await {
+                callback(&sg_root, &token_str, &name_str, &balance2);
+            }
+        });
+        app.tasks.lock().unwrap().push(listen_click);
+
+        let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+        layer.link(node);
+
+        // Token symbol
+        let node = create_text(&format!("token_{}", i));
+        let prop = node.get_property("rect").unwrap();
+        prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+        prop.set_f32(atom, Role::App, 1, *y + PADDING_Y).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+        prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+        node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+        node.set_property_str(atom, Role::App, "text", *token).unwrap();
+        let prop = node.get_property("text_color").unwrap();
+        if COLOR_SCHEME == ColorScheme::DarkMode {
+            prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+        } else {
+            prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+        }
+        node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+        let node = node
+            .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+            .await;
+        layer.link(node);
+
+        // Token name
+        let node = create_text(&format!("token_name_{}", i));
+        let prop = node.get_property("rect").unwrap();
+        prop.set_f32(atom, Role::App, 0, PADDING_X + TOKEN_NAME_OFFSET).unwrap();
+        prop.set_f32(atom, Role::App, 1, *y + PADDING_Y).unwrap();
+        prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
+        prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+        node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+        node.set_property_str(atom, Role::App, "text", *full_name).unwrap();
+        let prop = node.get_property("text_color").unwrap();
+        if COLOR_SCHEME == ColorScheme::DarkMode {
+            prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+        } else {
+            prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+        }
+        node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+        let node = node
+            .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+            .await;
+        layer.link(node);
+
+        // Balance
+        let node = create_text(&format!("token_balance_{}", i));
+        let prop = node.get_property("rect").unwrap();
+        let code = cc.compile("PADDING_X").unwrap();
+        prop.set_expr(atom, Role::App, 0, code).unwrap();
+        prop.set_f32(atom, Role::App, 1, *y + PADDING_Y).unwrap();
+        let code = cc.compile("w - PADDING_X * 2").unwrap();
+        prop.set_expr(atom, Role::App, 2, code).unwrap();
+        prop.set_f32(atom, Role::App, 3, BASE_FONTSIZE).unwrap();
+        node.set_property_enum(atom, Role::App, "text_align", "end").unwrap();
+        node.set_property_f32(atom, Role::App, "font_size", BASE_FONTSIZE).unwrap();
+        node.set_property_str(atom, Role::App, "text", balance.to_string()).unwrap();
+        let prop = node.get_property("text_color").unwrap();
+        if COLOR_SCHEME == ColorScheme::DarkMode {
+            prop.set_f32(atom, Role::App, 0, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 1, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 2, 1.).unwrap();
+            prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+        } else {
+            prop.set_f32(atom, Role::App, 0, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+            prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+        }
+        node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+        let node = node
+            .setup(|me| {
+                Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())
+            })
+            .await;
+        layer.link(node);
+
+        *y += PADDING_Y * 2. + BASE_FONTSIZE;
+
+        create_separator(app, atom, layer, &format!("token_separator_{}", i), y).await;
+    }
+}
+
+/// Creates a bottom button with teal outline and click handler.
+/// Returns the button node after setup, linked to the layer.
+/// The caller can use the returned button node to add custom click handlers.
+async fn create_bottom_button(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    name: &str,
+    cc: &mut expr::Compiler,
+    label_text: Option<&str>,
+    window_scale: &PropertyFloat32,
+    i18n_fish: &I18nBabelFish,
+) -> SceneNodePtr {
+    // Button bg (teal outline)
+    let node = create_vector_art(&format!("{}_bg", name));
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2.").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        COLOR_TEAL,
+    );
+    let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    layer.link(node.clone());
+
+    // Button label text (if provided)
+    if let Some(text) = label_text {
+        let label_node = create_text(&format!("{}_label", name));
+        let prop = label_node.get_property("rect").unwrap();
+        prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+        let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8").unwrap();
+        prop.set_expr(atom, Role::App, 1, code).unwrap();
+        let code = cc.compile("w - PADDING_X * 2.").unwrap();
+        prop.set_expr(atom, Role::App, 2, code).unwrap();
+        prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+        label_node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
+        label_node.set_property_str(atom, Role::App, "text", text).unwrap();
+        label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
+
+        let prop = label_node.get_property("text_color").unwrap();
+        prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+        prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+        prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+        prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+        label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+        let label_node = label_node
+            .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+            .await;
+        layer.link(label_node);
+    }
+
+    // Button
+    let node = create_button(name);
+    node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2.").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+
+    let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
+    layer.link(node.clone());
+    node
+}
+
+/// Creates a bottom button with two states: valid (teal) and invalid (grey).
+/// Returns a tuple of (button_node, bg_valid_node, bg_invalid_node, label_node) for visibility control.
+async fn create_bottom_button_with_states(
+    app: &App,
+    atom: &mut PropertyAtomicGuard,
+    layer: &SceneNodePtr,
+    name: &str,
+    cc: &mut expr::Compiler,
+    label_text: &str,
+    window_scale: &PropertyFloat32,
+    i18n_fish: &I18nBabelFish,
+    initial_valid: bool,
+) -> (SceneNodePtr, SceneNodePtr, SceneNodePtr, SceneNodePtr) {
+    // Button bg (teal outline - valid state)
+    let bg_valid = create_vector_art(&format!("{}_bg", name));
+    let prop = bg_valid.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2.").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    bg_valid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    bg_valid.set_property_bool(atom, Role::App, "is_visible", initial_valid).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        COLOR_TEAL,
+    );
+    let bg_valid = bg_valid.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    layer.link(bg_valid.clone());
+
+    // Button bg (grey outline - invalid state)
+    let bg_invalid = create_vector_art(&format!("{}_bg_grey", name));
+    let prop = bg_invalid.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2.").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    bg_invalid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
+    bg_invalid.set_property_bool(atom, Role::App, "is_visible", !initial_valid).unwrap();
+    let mut shape = VectorShape::new();
+    shape.add_outline(
+        expr::const_f32(0.),
+        expr::const_f32(0.),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        1.,
+        [0.5, 0.5, 0.5, 1.],
+    );
+    let bg_invalid = bg_invalid.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
+    layer.link(bg_invalid.clone());
+
+    // Button label text
+    let label_node = create_text(&format!("{}_label", name));
+    let prop = label_node.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2.").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+    label_node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
+    label_node.set_property_str(atom, Role::App, "text", label_text).unwrap();
+    label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
+
+    let prop = label_node.get_property("text_color").unwrap();
+    if initial_valid {
+        prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
+        prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
+        prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
+        prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
+    } else {
+        prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
+        prop.set_f32(atom, Role::App, 3, 1.).unwrap();
+    }
+    label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
+    let label_node = label_node
+        .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
+        .await;
+    layer.link(label_node.clone());
+
+    // Button
+    let btn = create_button(name);
+    btn.set_property_bool(atom, Role::App, "is_active", true).unwrap();
+    let prop = btn.get_property("rect").unwrap();
+    prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
+    let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
+    prop.set_expr(atom, Role::App, 1, code).unwrap();
+    let code = cc.compile("w - PADDING_X * 2.").unwrap();
+    prop.set_expr(atom, Role::App, 2, code).unwrap();
+    prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
+
+    let btn = btn.setup(|me| Button::new(me, app.renderer.clone())).await;
+    layer.link(btn.clone());
+
+    (btn, bg_valid, bg_invalid, label_node)
+}

+ 1 - 0
bin/app/src/mesh.rs

@@ -35,6 +35,7 @@ pub const COLOR_BLUE: Color = [0., 0., 1., 1.];
 #[allow(dead_code)]
 pub const COLOR_PINK: Color = [0.8, 0.3, 0.8, 1.];
 pub const COLOR_CYAN: Color = [0., 1., 1., 1.];
+pub const COLOR_TEAL: Color = [0.0784, 0.6824, 0.7216, 1.];
 #[allow(dead_code)]
 pub const COLOR_PURPLE: Color = [1., 0., 1., 1.];
 pub const COLOR_WHITE: Color = [1., 1., 1., 1.];

+ 1 - 1
bin/app/src/prop/mod.rs

@@ -34,7 +34,7 @@ pub use guard::{BatchGuardId, BatchGuardPtr, PropertyAtomicGuard};
 mod wrap;
 pub use wrap::{
     PropertyBool, PropertyColor, PropertyDimension, PropertyFloat32, PropertyRect, PropertyStr,
-    PropertyUint32,
+    PropertyUint32, PropertyEnum
 };
 
 #[derive(Debug, Copy, Clone, PartialEq, SerialEncodable, SerialDecodable)]

+ 42 - 4
bin/app/src/prop/wrap.rs

@@ -17,10 +17,7 @@
  */
 
 use crate::{
-    error::{Error, Result},
-    expr::{SExprMachine, SExprVal},
-    gfx::{Dimension, Rectangle},
-    scene::SceneNode as SceneNode3,
+    error::{Error, Result}, expr::{SExprMachine, SExprVal}, gfx::{Dimension, Rectangle}, prop::PropertyType, scene::SceneNode as SceneNode3
 };
 
 use super::{PropertyAtomicGuard, PropertyPtr, Role};
@@ -236,6 +233,47 @@ impl PropertyDimension {
     }
 }
 
+#[derive(Clone)]
+pub struct PropertyEnum {
+    prop: PropertyPtr,
+    role: Role,
+    idx: usize,
+}
+
+impl PropertyEnum {
+    pub fn wrap(node: &SceneNode3, role: Role, prop_name: &str, idx: usize) -> Result<Self> {
+        let prop = node.get_property(prop_name).ok_or(Error::PropertyNotFound)?;
+
+        // Verify property type is Enum
+        if prop.typ != PropertyType::Enum {
+            return Err(Error::PropertyWrongType)
+        }
+
+        Ok(Self { prop, role, idx })
+    }
+
+    pub fn get(&self) -> String {
+        // Handle both PropertyValue::Str (defaults) and PropertyValue::Enum (set values)
+        match self.prop.get_value(self.idx) {
+            Ok(val) => {
+                match val.as_str() {
+                    Ok(s) => s,
+                    Err(_) => val.as_enum().unwrap(),
+                }
+            }
+            Err(_) => self.prop.get_enum(self.idx).unwrap(),
+        }
+    }
+
+    pub fn set<S: Into<String>>(&self, atom: &mut PropertyAtomicGuard, val: S) {
+        self.prop().set_enum(atom, self.role, self.idx, val.into()).unwrap()
+    }
+
+    #[inline]
+    pub fn prop(&self) -> PropertyPtr {
+        self.prop.clone()
+    }
+}
 /*
 #[derive(Clone)]
 pub struct PropertyPoint {

+ 9 - 0
bin/app/src/scene.rs

@@ -311,6 +311,15 @@ impl SceneNode {
     ) -> Result<()> {
         self.get_property(name).ok_or(Error::PropertyNotFound)?.set_str(atom, role, 0, val)
     }
+    pub fn set_property_enum<S: Into<String>>(
+        &self,
+        atom: &mut PropertyAtomicGuard,
+        role: Role,
+        name: &str,
+        val: S,
+    ) -> Result<()> {
+        self.get_property(name).ok_or(Error::PropertyNotFound)?.set_enum(atom, role, 0, val)
+    }
     pub fn set_property_node_id(
         &self,
         atom: &mut PropertyAtomicGuard,

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 416 - 0
bin/app/src/shape/copy.rs


+ 3 - 0
bin/app/src/shape/mod.rs

@@ -38,6 +38,9 @@ pub use netlogo3::create_netlogo3;
 mod version_block;
 pub use version_block::create_version_block;
 
+mod copy;
+pub use copy::create_copy;
+
 //mod settings;
 //pub use settings::{create_right_border, create_settings};
 

+ 21 - 3
bin/app/src/text/mod.rs

@@ -65,7 +65,7 @@ pub fn make_layout(
     width: Option<f32>,
     underlines: &[Range<usize>],
 ) -> parley::Layout<Color> {
-    make_layout2(text, text_color, font_size, lineheight, window_scale, width, underlines, &[])
+    make_layout2(text, text_color, font_size, lineheight, window_scale, width, underlines, &[], "start", "normal")
 }
 
 pub fn make_layout2(
@@ -77,8 +77,26 @@ pub fn make_layout2(
     width: Option<f32>,
     underlines: &[Range<usize>],
     foreground_colors: &[(Range<usize>, Color)],
+    text_align: &str,
+    overflow_wrap: &str,
 ) -> parley::Layout<Color> {
     THREAD_LAYOUT_CTX.with(|layout_ctx| {
+        let text_align = match text_align {
+            "start" => parley::Alignment::Start,
+            "end" => parley::Alignment::End,
+            "left" => parley::Alignment::Left,
+            "center" => parley::Alignment::Center,
+            "right" => parley::Alignment::Right,
+            "justify" => parley::Alignment::Justify,
+            _ => parley::Alignment::Start,
+        };
+        let overflow_wrap = match overflow_wrap {
+            "normal" => parley::OverflowWrap::Normal,
+            "anywhere" => parley::OverflowWrap::Anywhere,
+            "break-word" => parley::OverflowWrap::BreakWord,
+            _ => parley::OverflowWrap::Normal,
+        };
+
         let mut layout_ctx = layout_ctx.borrow_mut();
         let mut font_ctx = GLOBAL_FONT_CTX.clone();
 
@@ -87,7 +105,7 @@ pub fn make_layout2(
         builder.push_default(parley::StyleProperty::FontSize(font_size));
         builder.push_default(parley::StyleProperty::from(FONT_STACK));
         builder.push_default(parley::StyleProperty::Brush(text_color));
-        builder.push_default(parley::StyleProperty::OverflowWrap(parley::OverflowWrap::Anywhere));
+        builder.push_default(parley::StyleProperty::OverflowWrap(overflow_wrap));
 
         for underline in underlines {
             builder.push(parley::StyleProperty::Underline(true), underline.clone());
@@ -99,7 +117,7 @@ pub fn make_layout2(
 
         let mut layout: parley::Layout<Color> = builder.build(text);
         layout.break_all_lines(width);
-        layout.align(width, parley::Alignment::Start, parley::AlignmentOptions::default());
+        layout.align(width, text_align, parley::AlignmentOptions::default());
         layout
     })
 }

+ 2 - 0
bin/app/src/ui/chatview/page.rs

@@ -155,6 +155,8 @@ impl PrivMessage {
                 Some(clip.w - timestamp_width),
                 &[],
                 &[(0..nick_end, nick_color)],
+                "start",
+                "normal",
             )
         };
         self.txt_layout = Some(txt_layout);

+ 12 - 0
bin/app/src/ui/edit/mod.rs

@@ -397,6 +397,18 @@ impl BaseEdit {
         self.node.upgrade().unwrap()
     }
 
+    /// Reset the horizontal scroll to 0.
+    /// Useful when the input width is dynamically changed and scrolling is not desired.
+    pub fn reset_scroll(&self) {
+        self.scroll.store(0., Ordering::Release);
+    }
+
+    /// Update the editor when the text property was changed externally.
+    /// This should be called after setting the text property directly.
+    pub fn on_text_prop_changed(&self) {
+        self.editor.lock().on_text_prop_changed();
+    }
+
     fn abs_to_local(&self, point: &mut Point) {
         let rect = self.rect.get();
         *point -= rect.pos();

+ 24 - 14
bin/app/src/ui/text.rs

@@ -23,16 +23,10 @@ use std::sync::Arc;
 use tracing::instrument;
 
 use crate::{
-    gfx::{gfxtag, DrawCall, DrawInstruction, Rectangle, RenderApi, Renderer},
-    mesh::MeshBuilder,
-    prop::{
+    ExecutorPtr, gfx::{DrawCall, DrawInstruction, Rectangle, RenderApi, Renderer, gfxtag}, mesh::{Color, MeshBuilder}, prop::{
         BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32,
-        PropertyRect, PropertyStr, PropertyUint32, Role,
-    },
-    scene::{Pimpl, SceneNodeWeak},
-    text,
-    util::i18n::I18nBabelFish,
-    ExecutorPtr,
+        PropertyRect, PropertyStr, PropertyUint32, PropertyEnum, Role,
+    }, scene::{Pimpl, SceneNodeWeak}, text, util::i18n::I18nBabelFish
 };
 
 use super::{DrawUpdate, OnModify, UIObject};
@@ -48,12 +42,15 @@ pub struct Text {
     dc_key: u64,
 
     rect: PropertyRect,
+    height: PropertyFloat32,
     z_index: PropertyUint32,
     priority: PropertyUint32,
     text: PropertyStr,
     font_size: PropertyFloat32,
     text_color: PropertyColor,
     lineheight: PropertyFloat32,
+    text_align: PropertyEnum,
+    overflow_wrap: PropertyEnum,
     use_i18n: PropertyBool,
     debug: PropertyBool,
 
@@ -70,12 +67,15 @@ impl Text {
     ) -> Pimpl {
         let node_ref = &node.upgrade().unwrap();
         let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
+        let height = PropertyFloat32::wrap(node_ref, Role::Internal, "height", 0).unwrap();
         let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
         let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
         let text = PropertyStr::wrap(node_ref, Role::Internal, "text", 0).unwrap();
         let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
         let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
         let lineheight = PropertyFloat32::wrap(node_ref, Role::Internal, "lineheight", 0).unwrap();
+        let text_align = PropertyEnum::wrap(node_ref, Role::Internal, "text_align", 0).unwrap();
+        let overflow_wrap = PropertyEnum::wrap(node_ref, Role::Internal, "overflow_wrap", 0).unwrap();
         let use_i18n = PropertyBool::wrap(node_ref, Role::Internal, "use_i18n", 0).unwrap();
         let debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap();
 
@@ -87,12 +87,15 @@ impl Text {
             dc_key: OsRng.gen(),
 
             rect,
+            height,
             z_index,
             priority,
             text,
             font_size,
             text_color,
             lineheight,
+            text_align,
+            overflow_wrap,
             use_i18n,
             debug,
 
@@ -103,12 +106,15 @@ impl Text {
         Pimpl::Text(self_)
     }
 
-    fn regen_mesh(&self) -> Vec<DrawInstruction> {
+    fn make_layout(&self) -> parley::Layout<Color> {
         let text = self.text.get();
         let font_size = self.font_size.get();
         let lineheight = self.lineheight.get();
         let text_color = self.text_color.get();
         let window_scale = self.window_scale.get();
+        let width = self.rect.get_width();
+        let text_align = self.text_align.get();
+        let overflow_wrap = self.overflow_wrap.get();
 
         let text = if self.use_i18n.get() {
             if let Some(trans) = self.i18n_fish.tr(&text) {
@@ -121,15 +127,16 @@ impl Text {
             text
         };
 
-        let layout =
-            text::make_layout(&text, text_color, font_size, lineheight, window_scale, None, &[]);
+        text::make_layout2(&text, text_color, font_size, lineheight, window_scale, Some(width), &[], &[], &text_align, &overflow_wrap)
+    }
 
+    fn regen_mesh(&self, layout: &parley::Layout<Color>) -> Vec<DrawInstruction> {
         let mut debug_opts = text::DebugRenderOptions::OFF;
         if self.debug.get() {
             debug_opts |= text::DebugRenderOptions::BASELINE;
         }
 
-        text::render_layout_with_opts(&layout, debug_opts, &self.renderer, gfxtag!("text"))
+        text::render_layout_with_opts(layout, debug_opts, &self.renderer, gfxtag!("text"))
     }
 
     #[instrument(target = "ui::text")]
@@ -155,8 +162,11 @@ impl Text {
         self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
 
+        let layout = self.make_layout();
+        self.height.set(atom, layout.height());
+
         let mut instrs = vec![DrawInstruction::Move(rect.pos())];
-        instrs.append(&mut self.regen_mesh());
+        instrs.append(&mut self.regen_mesh(&layout));
 
         if self.debug.get() {
             let rect = self.rect.get().with_zero_pos();

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio