util.rs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use darkfi_money_contract::model::TokenId;
  19. use darkfi_serial::Decodable;
  20. use crate::{
  21. app::App,
  22. app::node::{create_button, create_text, create_vector_art},
  23. app::schema::COLOR_SCHEME,
  24. expr::{self, Compiler},
  25. gfx::Renderer,
  26. mesh::{COLOR_TEAL, COLOR_CYAN},
  27. prop::{PropertyAtomicGuard, PropertyFloat32, Role},
  28. scene::SceneNodePtr,
  29. scene::Pimpl,
  30. ui::{Button, Text, VectorArt, VectorShape},
  31. util::i18n::I18nBabelFish,
  32. };
  33. use super::{super::ColorScheme, data::*};
  34. pub fn is_valid_address(address: &str) -> bool {
  35. address.len() > 3 // TODO
  36. }
  37. pub async fn get_balance(sg_root: &SceneNodePtr, token_id: &TokenId) -> f32 {
  38. let Some(drk_node) = sg_root.lookup_node("/plugin/drk") else {
  39. return 0.
  40. };
  41. let Ok(Some(response_data)) = drk_node.call_method("get_balances", vec![]).await else {
  42. return 0.
  43. };
  44. let mut cur = std::io::Cursor::new(response_data);
  45. let Ok(balances) = Vec::<(String, TokenId, f32)>::decode(&mut cur) else {
  46. return 0.
  47. };
  48. balances
  49. .iter()
  50. .find(|(_, tid, _)| *tid == *token_id)
  51. .map(|(_, _, balance)| *balance)
  52. .unwrap_or(0.)
  53. }
  54. /// Update positions for amount input wrapper and token symbol to center them together.
  55. pub async fn update_amount_screen(
  56. atom: &mut PropertyAtomicGuard,
  57. sg_root: &SceneNodePtr,
  58. amount_text: &str,
  59. token_id: &TokenId,
  60. token_symbol: &str,
  61. amount_wrapper_node: &SceneNodePtr,
  62. amount_input_node: &SceneNodePtr,
  63. token_node: &SceneNodePtr,
  64. available_balance_node: Option<&SceneNodePtr>,
  65. ) {
  66. let mut cc = expr::Compiler::new();
  67. let display_text = if amount_text.is_empty() { "0" } else { amount_text };
  68. let char_count = display_text.chars().count() as f32;
  69. let token_char_count = token_symbol.chars().count() as f32;
  70. let text_width = char_count * AMOUNT_CHAR_WIDTH + 6.;
  71. let token_width = token_char_count * AMOUNT_CHAR_WIDTH;
  72. let total_width = text_width + AMOUNT_TOKEN_SPACING + token_width;
  73. cc.add_const_f32("AMOUNT_TOTAL_WIDTH", total_width);
  74. cc.add_const_f32("AMOUNT_WIDTH", text_width);
  75. cc.add_const_f32("AMOUNT_TOKEN_SPACING", AMOUNT_TOKEN_SPACING);
  76. cc.add_const_f32("AMOUNT_FONTSIZE", AMOUNT_FONTSIZE);
  77. let wrapper_rect = amount_wrapper_node.get_property("rect").unwrap();
  78. wrapper_rect.set_expr(atom, Role::App, 0, cc.compile("(w - AMOUNT_TOTAL_WIDTH) / 2").unwrap()).unwrap();
  79. let width_code = cc.compile("AMOUNT_WIDTH").unwrap();
  80. let amount_rect = amount_input_node.get_property("rect").unwrap();
  81. amount_rect.set_expr(atom, Role::App, 2, width_code).unwrap();
  82. // Reset scroll to prevent content from being cropped
  83. if let Pimpl::Edit(edit) = amount_input_node.pimpl() {
  84. edit.reset_scroll();
  85. }
  86. // Update token symbol position
  87. let token_rect = token_node.get_property("rect").unwrap();
  88. token_rect.set_expr(atom, Role::App, 0, cc.compile("(w - AMOUNT_TOTAL_WIDTH) / 2 + AMOUNT_TOKEN_SPACING + AMOUNT_WIDTH").unwrap()).unwrap();
  89. // Set available balance
  90. if let Some(available_balance_node) = available_balance_node {
  91. let available_balance = get_balance(sg_root, token_id).await;
  92. available_balance_node.set_property_str(atom, Role::App, "text", format!("{available_balance} available")).unwrap();
  93. }
  94. }
  95. /// Creates a title text node with separator line.
  96. /// Returns the text node after setup, linked to the layer
  97. pub async fn create_title(
  98. app: &App,
  99. atom: &mut PropertyAtomicGuard,
  100. layer: &SceneNodePtr,
  101. window_scale: &PropertyFloat32,
  102. i18n_fish: &I18nBabelFish,
  103. name: &str,
  104. y: &mut f32,
  105. ) -> SceneNodePtr {
  106. let node = create_text(name);
  107. let prop = node.get_property("rect").unwrap();
  108. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  109. prop.set_f32(atom, Role::App, 1, *y + TITLE_PADDING).unwrap();
  110. prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
  111. prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
  112. node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
  113. node.set_property_str(atom, Role::App, "text", name).unwrap();
  114. let prop = node.get_property("text_color").unwrap();
  115. if COLOR_SCHEME == ColorScheme::DarkMode {
  116. prop.set_f32(atom, Role::App, 0, 1.).unwrap();
  117. prop.set_f32(atom, Role::App, 1, 1.).unwrap();
  118. prop.set_f32(atom, Role::App, 2, 1.).unwrap();
  119. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  120. } else {
  121. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  122. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  123. prop.set_f32(atom, Role::App, 2, 0.).unwrap();
  124. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  125. }
  126. node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  127. let node = node.setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())).await;
  128. layer.link(node.clone());
  129. *y += TITLE_PADDING * 2. + TITLE_FONTSIZE + 1.;
  130. create_separator(&app.renderer, atom, layer, &format!("{}_separator", name), y).await;
  131. node
  132. }
  133. /// Creates a subtitle text node with separator line (e.g., "Pick a token to send", "Recipient", "Address")
  134. /// Returns the text node after setup, linked to the layer
  135. pub async fn create_subtitle(
  136. app: &App,
  137. atom: &mut PropertyAtomicGuard,
  138. layer: &SceneNodePtr,
  139. window_scale: &PropertyFloat32,
  140. i18n_fish: &I18nBabelFish,
  141. name: &str,
  142. text: &str,
  143. y: &mut f32,
  144. ) -> SceneNodePtr {
  145. let node = create_text(name);
  146. let prop = node.get_property("rect").unwrap();
  147. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  148. prop.set_f32(atom, Role::App, 1, *y + PADDING_Y).unwrap();
  149. prop.set_f32(atom, Role::App, 2, 1000.).unwrap();
  150. prop.set_f32(atom, Role::App, 3, TITLE_FONTSIZE).unwrap();
  151. node.set_property_f32(atom, Role::App, "font_size", TITLE_FONTSIZE).unwrap();
  152. node.set_property_str(atom, Role::App, "text", text).unwrap();
  153. let prop = node.get_property("text_color").unwrap();
  154. if COLOR_SCHEME == ColorScheme::DarkMode {
  155. prop.set_f32(atom, Role::App, 0, 1.).unwrap();
  156. prop.set_f32(atom, Role::App, 1, 1.).unwrap();
  157. prop.set_f32(atom, Role::App, 2, 1.).unwrap();
  158. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  159. } else {
  160. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  161. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  162. prop.set_f32(atom, Role::App, 2, 0.).unwrap();
  163. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  164. }
  165. node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  166. let node = node.setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone())).await;
  167. layer.link(node.clone());
  168. *y += PADDING_Y * 2. + TITLE_FONTSIZE + 1.;
  169. create_separator(&app.renderer, atom, layer, &format!("{}_separator", text), y).await;
  170. node
  171. }
  172. /// Creates a background mesh with gradient box.
  173. pub async fn create_bg_mesh(
  174. app: &App,
  175. atom: &mut PropertyAtomicGuard,
  176. layer: &SceneNodePtr,
  177. name: &str,
  178. ) {
  179. let node = create_vector_art(name);
  180. let prop = node.get_property("rect").unwrap();
  181. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  182. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  183. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  184. prop.set_expr(atom, Role::App, 3, expr::load_var("h")).unwrap();
  185. node.set_property_u32(atom, Role::App, "z_index", 0).unwrap();
  186. let mut shape = VectorShape::new();
  187. shape.add_gradient_box(
  188. expr::const_f32(0.),
  189. expr::const_f32(0.),
  190. expr::load_var("w"),
  191. expr::load_var("h"),
  192. [[0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.8]],
  193. );
  194. let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
  195. layer.link(node);
  196. }
  197. /// Creates a header background with filled box and separator line at bottom.
  198. pub async fn create_header_bg(
  199. app: &App,
  200. atom: &mut PropertyAtomicGuard,
  201. layer: &SceneNodePtr,
  202. name: &str,
  203. ) {
  204. let node = create_vector_art(name);
  205. let prop = node.get_property("rect").unwrap();
  206. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  207. prop.set_f32(atom, Role::App, 1, 0.).unwrap();
  208. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  209. prop.set_f32(atom, Role::App, 3, HEADER_HEIGHT).unwrap();
  210. node.set_property_u32(atom, Role::App, "z_index", 1).unwrap();
  211. let (bg_color, sep_color) = match COLOR_SCHEME {
  212. ColorScheme::DarkMode => ([0., 0.11, 0.11, 1.], [0.2, 0.2745, 0.2784, 1.]),
  213. ColorScheme::PaperLight => ([1., 1., 1., 1.], [0., 0.6, 0.65, 1.]),
  214. };
  215. let mut shape = VectorShape::new();
  216. shape.add_filled_box(
  217. expr::const_f32(0.),
  218. expr::const_f32(0.),
  219. expr::load_var("w"),
  220. expr::const_f32(HEADER_HEIGHT),
  221. bg_color,
  222. );
  223. shape.add_filled_box(
  224. expr::const_f32(0.),
  225. expr::const_f32(HEADER_HEIGHT - 1.),
  226. expr::load_var("w"),
  227. expr::const_f32(HEADER_HEIGHT),
  228. sep_color,
  229. );
  230. let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
  231. layer.link(node);
  232. }
  233. /// Creates a separator line at the given y expression.
  234. /// Returns the separator node after setup, linked to the layer
  235. pub async fn create_separator_expr(
  236. app: &App,
  237. atom: &mut PropertyAtomicGuard,
  238. layer: &SceneNodePtr,
  239. name: &str,
  240. cc: &mut Compiler,
  241. y_expr: &str,
  242. ) -> SceneNodePtr {
  243. let sep_node = create_vector_art(name);
  244. let prop = sep_node.get_property("rect").unwrap();
  245. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  246. let code = cc.compile(y_expr).unwrap();
  247. prop.set_expr(atom, Role::App, 1, code).unwrap();
  248. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  249. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  250. sep_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  251. let mut shape = VectorShape::new();
  252. shape.add_filled_box(
  253. expr::const_f32(0.),
  254. expr::const_f32(0.),
  255. expr::load_var("w"),
  256. expr::const_f32(1.),
  257. [0.2, 0.2745, 0.2784, 1.],
  258. );
  259. let sep_node = sep_node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
  260. layer.link(sep_node.clone());
  261. sep_node
  262. }
  263. /// Creates a separator line at the current y position and increments y.
  264. /// Returns the separator node after setup, linked to the layer
  265. pub async fn create_separator(
  266. renderer: &Renderer,
  267. atom: &mut PropertyAtomicGuard,
  268. layer: &SceneNodePtr,
  269. name: &str,
  270. y: &mut f32,
  271. ) -> SceneNodePtr {
  272. let sep_node = create_vector_art(name);
  273. let prop = sep_node.get_property("rect").unwrap();
  274. prop.set_f32(atom, Role::App, 0, 0.).unwrap();
  275. prop.set_f32(atom, Role::App, 1, *y).unwrap();
  276. prop.set_expr(atom, Role::App, 2, expr::load_var("w")).unwrap();
  277. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  278. sep_node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  279. let mut shape = VectorShape::new();
  280. shape.add_filled_box(
  281. expr::const_f32(0.),
  282. expr::const_f32(0.),
  283. expr::load_var("w"),
  284. expr::const_f32(1.),
  285. [0.2, 0.2745, 0.2784, 1.],
  286. );
  287. let sep_node = sep_node.setup(|me| VectorArt::new(me, shape, renderer.clone())).await;
  288. layer.link(sep_node.clone());
  289. *y += 1.;
  290. sep_node
  291. }
  292. /// Creates a bottom button with teal outline and click handler.
  293. /// Returns the button node after setup, linked to the layer.
  294. /// The caller can use the returned button node to add custom click handlers.
  295. pub async fn create_bottom_button(
  296. app: &App,
  297. atom: &mut PropertyAtomicGuard,
  298. layer: &SceneNodePtr,
  299. name: &str,
  300. cc: &mut Compiler,
  301. label_text: Option<&str>,
  302. window_scale: &PropertyFloat32,
  303. i18n_fish: &I18nBabelFish,
  304. ) -> SceneNodePtr {
  305. let node = create_vector_art(&format!("{}_bg", name));
  306. let prop = node.get_property("rect").unwrap();
  307. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  308. let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
  309. prop.set_expr(atom, Role::App, 1, code).unwrap();
  310. let code = cc.compile("w - PADDING_X * 2.").unwrap();
  311. prop.set_expr(atom, Role::App, 2, code).unwrap();
  312. prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
  313. node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  314. node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
  315. let mut shape = VectorShape::new();
  316. shape.add_outline(
  317. expr::const_f32(0.),
  318. expr::const_f32(0.),
  319. expr::load_var("w"),
  320. expr::load_var("h"),
  321. 1.,
  322. COLOR_TEAL,
  323. );
  324. let node = node.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
  325. layer.link(node.clone());
  326. // Button label text (if provided)
  327. if let Some(text) = label_text {
  328. let label_node = create_text(&format!("{}_label", name));
  329. let prop = label_node.get_property("rect").unwrap();
  330. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  331. let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8").unwrap();
  332. prop.set_expr(atom, Role::App, 1, code).unwrap();
  333. let code = cc.compile("w - PADDING_X * 2.").unwrap();
  334. prop.set_expr(atom, Role::App, 2, code).unwrap();
  335. prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
  336. label_node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
  337. label_node.set_property_str(atom, Role::App, "text", text).unwrap();
  338. label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
  339. let prop = label_node.get_property("text_color").unwrap();
  340. prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
  341. prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
  342. prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
  343. prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
  344. label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
  345. let label_node = label_node
  346. .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
  347. .await;
  348. layer.link(label_node);
  349. }
  350. // Button
  351. let node = create_button(name);
  352. node.set_property_bool(atom, Role::App, "is_active", true).unwrap();
  353. let prop = node.get_property("rect").unwrap();
  354. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  355. let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
  356. prop.set_expr(atom, Role::App, 1, code).unwrap();
  357. let code = cc.compile("w - PADDING_X * 2.").unwrap();
  358. prop.set_expr(atom, Role::App, 2, code).unwrap();
  359. prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
  360. let node = node.setup(|me| Button::new(me, app.renderer.clone())).await;
  361. layer.link(node.clone());
  362. node
  363. }
  364. /// Creates a bottom button with two states: valid (teal) and invalid (grey).
  365. /// Returns a tuple of (button_node, bg_valid_node, bg_invalid_node, label_node) for visibility control.
  366. pub async fn create_bottom_button_with_states(
  367. app: &App,
  368. atom: &mut PropertyAtomicGuard,
  369. layer: &SceneNodePtr,
  370. name: &str,
  371. cc: &mut Compiler,
  372. label_text: &str,
  373. window_scale: &PropertyFloat32,
  374. i18n_fish: &I18nBabelFish,
  375. initial_valid: bool,
  376. ) -> (SceneNodePtr, SceneNodePtr, SceneNodePtr, SceneNodePtr) {
  377. // Button bg (teal outline - valid state)
  378. let bg_valid = create_vector_art(&format!("{}_bg", name));
  379. let prop = bg_valid.get_property("rect").unwrap();
  380. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  381. let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
  382. prop.set_expr(atom, Role::App, 1, code).unwrap();
  383. let code = cc.compile("w - PADDING_X * 2.").unwrap();
  384. prop.set_expr(atom, Role::App, 2, code).unwrap();
  385. prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
  386. bg_valid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  387. bg_valid.set_property_bool(atom, Role::App, "is_visible", initial_valid).unwrap();
  388. let mut shape = VectorShape::new();
  389. shape.add_outline(
  390. expr::const_f32(0.),
  391. expr::const_f32(0.),
  392. expr::load_var("w"),
  393. expr::load_var("h"),
  394. 1.,
  395. COLOR_TEAL,
  396. );
  397. let bg_valid = bg_valid.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
  398. layer.link(bg_valid.clone());
  399. // Button bg (grey outline - invalid state)
  400. let bg_invalid = create_vector_art(&format!("{}_bg_grey", name));
  401. let prop = bg_invalid.get_property("rect").unwrap();
  402. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  403. let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
  404. prop.set_expr(atom, Role::App, 1, code).unwrap();
  405. let code = cc.compile("w - PADDING_X * 2.").unwrap();
  406. prop.set_expr(atom, Role::App, 2, code).unwrap();
  407. prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
  408. bg_invalid.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  409. bg_invalid.set_property_bool(atom, Role::App, "is_visible", !initial_valid).unwrap();
  410. let mut shape = VectorShape::new();
  411. shape.add_outline(
  412. expr::const_f32(0.),
  413. expr::const_f32(0.),
  414. expr::load_var("w"),
  415. expr::load_var("h"),
  416. 1.,
  417. [0.5, 0.5, 0.5, 1.],
  418. );
  419. let bg_invalid = bg_invalid.setup(|me| VectorArt::new(me, shape, app.renderer.clone())).await;
  420. layer.link(bg_invalid.clone());
  421. // Button label text
  422. let label_node = create_text(&format!("{}_label", name));
  423. let prop = label_node.get_property("rect").unwrap();
  424. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  425. let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT + BUTTON_HEIGHT / 2 - BUTTON_FONTSIZE / 1.8").unwrap();
  426. prop.set_expr(atom, Role::App, 1, code).unwrap();
  427. let code = cc.compile("w - PADDING_X * 2.").unwrap();
  428. prop.set_expr(atom, Role::App, 2, code).unwrap();
  429. prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
  430. label_node.set_property_f32(atom, Role::App, "font_size", BUTTON_FONTSIZE).unwrap();
  431. label_node.set_property_str(atom, Role::App, "text", label_text).unwrap();
  432. label_node.set_property_enum(atom, Role::App, "text_align", "center").unwrap();
  433. let prop = label_node.get_property("text_color").unwrap();
  434. if initial_valid {
  435. prop.set_f32(atom, Role::App, 0, COLOR_CYAN[0]).unwrap();
  436. prop.set_f32(atom, Role::App, 1, COLOR_CYAN[1]).unwrap();
  437. prop.set_f32(atom, Role::App, 2, COLOR_CYAN[2]).unwrap();
  438. prop.set_f32(atom, Role::App, 3, COLOR_CYAN[3]).unwrap();
  439. } else {
  440. prop.set_f32(atom, Role::App, 0, 0.5).unwrap();
  441. prop.set_f32(atom, Role::App, 1, 0.5).unwrap();
  442. prop.set_f32(atom, Role::App, 2, 0.5).unwrap();
  443. prop.set_f32(atom, Role::App, 3, 1.).unwrap();
  444. }
  445. label_node.set_property_u32(atom, Role::App, "z_index", 3).unwrap();
  446. let label_node = label_node
  447. .setup(|me| Text::new(me, window_scale.clone(), app.renderer.clone(), i18n_fish.clone()))
  448. .await;
  449. layer.link(label_node.clone());
  450. // Button
  451. let btn = create_button(name);
  452. btn.set_property_bool(atom, Role::App, "is_active", true).unwrap();
  453. let prop = btn.get_property("rect").unwrap();
  454. prop.set_f32(atom, Role::App, 0, PADDING_X).unwrap();
  455. let code = cc.compile("h - PADDING_X - BUTTON_HEIGHT").unwrap();
  456. prop.set_expr(atom, Role::App, 1, code).unwrap();
  457. let code = cc.compile("w - PADDING_X * 2.").unwrap();
  458. prop.set_expr(atom, Role::App, 2, code).unwrap();
  459. prop.set_f32(atom, Role::App, 3, BUTTON_HEIGHT).unwrap();
  460. let btn = btn.setup(|me| Button::new(me, app.renderer.clone())).await;
  461. layer.link(btn.clone());
  462. (btn, bg_valid, bg_invalid, label_node)
  463. }