scifi.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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. //! The `scifi` theme: the cyan-token look, king video background,
  19. //! first-run scramble splash, and the netstatus overlay fade — the
  20. //! shipped dark.fi app look, extracted from the schema (task 7.1).
  21. //!
  22. //! Everything here goes through the `ThemeCtx`, so unload restores the
  23. //! minimal baseline for every touched property, removes journaled
  24. //! dependency edges, cancels the fade watcher and the splash hide task,
  25. //! and unlinks the video/splash/token nodes — atomically, as one batch.
  26. use darkfi::system::msleep;
  27. use indoc::indoc;
  28. use std::sync::Arc;
  29. use crate::{
  30. app::{
  31. node::{create_vector_art, create_video},
  32. schema::{
  33. wallet::data::{AMOUNT_FONTSIZE, BASE_FONTSIZE, CHAT_BTN_MARGIN, CHAT_BTN_SIZE},
  34. MAIN_MENU_HEADER_HEIGHT, VID_ASPECT_RATIO, VID_PATH,
  35. },
  36. },
  37. error::{Error, Result},
  38. expr::{self, Compiler},
  39. gfx::gfxtag,
  40. prop::{
  41. Property, PropertyAtomicGuard, PropertyFloat32, PropertyPermission, PropertySubType,
  42. PropertyType, PropertyValue, Role,
  43. },
  44. scene::{SceneNode, SceneNodeType},
  45. theme::{Theme, ThemeCtx},
  46. ui::{VectorArt, VectorShape, Video},
  47. };
  48. pub struct ScifiTheme;
  49. #[async_trait::async_trait]
  50. impl Theme for ScifiTheme {
  51. fn name(&self) -> &'static str {
  52. "scifi"
  53. }
  54. async fn apply(&self, ctx: &ThemeCtx) -> Result<()> {
  55. scifi_apply(ctx).await
  56. }
  57. }
  58. /// scifi's private token vocabulary (design D6): exists only while the
  59. /// theme is applied, referenced from theme-installed vals expressions
  60. /// and journaled overrides — never from schema defaults.
  61. fn private_token_props() -> Vec<Property> {
  62. let mut label_glow = Property::new(
  63. "label_glow",
  64. PropertyType::Float32,
  65. PropertySubType::Color,
  66. PropertyPermission { read: Role::ALL, write: Role::Theme },
  67. );
  68. label_glow.set_array_len(4);
  69. label_glow.set_defaults_f32(vec![0.47, 1., 0.75, 1.]).unwrap();
  70. vec![label_glow]
  71. }
  72. async fn scifi_apply(ctx: &ThemeCtx) -> Result<()> {
  73. let app = ctx.app().unwrap();
  74. let atom = &mut PropertyAtomicGuard::none();
  75. let cc = Compiler::new();
  76. // ------------------------------------------------------------------
  77. // 1. Shared token values: the cyan palette over the minimal baseline
  78. // ------------------------------------------------------------------
  79. let tokens: &[(&str, [f32; 4])] = &[
  80. ("text_color", [1., 1., 1., 1.]),
  81. ("edit.text_color", [1., 1., 1., 1.]),
  82. ("edit.hi_bg_color", [0., 0.27, 0.22, 1.]),
  83. ("edit.text_hi_color", [0.44, 0.96, 1., 1.]),
  84. ("edit.cursor_color", [0.816, 0.627, 1., 1.]),
  85. ("edit.action_fg_color", [0., 0.94, 1., 1.]),
  86. ("edit.action_bg_color", [0.1, 0.1, 0.1, 0.9]),
  87. ("menu.bg_color", [0., 0., 0., 0.5]),
  88. //("menu.role1_color", [0.36, 1., 0.51, 1.]),
  89. //("menu.role2_color", [0.56, 0.61, 1., 1.]),
  90. ("chatview.timestamp_color", [0.407, 0.604, 0.647, 1.]),
  91. ("chatview.text_color", [1., 1., 1., 1.]),
  92. ("chatview.hi_bg_color", [0., 0.2, 0.2, 1.]),
  93. ("chatview.action_text_color", [0.5, 0.25, 0.75, 1.]),
  94. ("chatview.url_text_color", [0., 0.94, 1., 1.]),
  95. ("wallet.text_color", [1., 1., 1., 1.]),
  96. ("wallet.title_color", [0.6471, 0.8667, 0.8157, 1.]),
  97. ("wallet.label_color", [0.6902, 1., 1., 1.]),
  98. ("wallet.accent_color", [0., 1., 1., 1.]),
  99. ("wallet.value_color", [0.5451, 1., 1., 1.]),
  100. ("wallet.muted_color", [0.3686, 0.6, 0.5451, 1.]),
  101. ("wallet.recipient_color", [0.3647, 1., 0.6941, 1.]),
  102. ("wallet.tx_status_recipient_color", [0., 1., 0.6392, 1.]),
  103. ("wallet.tx_status_value_color", [0.5451, 1., 1., 1.]),
  104. ("wallet.fee_label_color", [1., 1., 1., 1.]),
  105. ("wallet.fee_value_color", [0.8157, 0.6275, 1., 1.]),
  106. ];
  107. for (name, val) in tokens {
  108. ctx.set_shared_token_color(atom, name, *val)?;
  109. }
  110. // ------------------------------------------------------------------
  111. // 2. Private tokens under /theme/scifi
  112. // ------------------------------------------------------------------
  113. let token_child = ctx.create_token_child("scifi", private_token_props())?;
  114. let label_glow = token_child.get_property("label_glow").unwrap();
  115. // Private-token proof: the netstatus overlay's P2P/OUTBOUND labels
  116. // follow `label_glow` through journaled edges (removed on unload).
  117. for label in ["p2p_label", "outbound_label"] {
  118. let Some(node) =
  119. ctx.sg_root().lookup_node(&format!("/window/content/chat/netstatus_overlay/{label}"))
  120. else {
  121. continue;
  122. };
  123. let Some(prop) = node.get_property("text_color") else { continue };
  124. for i in 0..4 {
  125. let local = format!("th_label_glow_{i}");
  126. ctx.depend_journaled(&prop, &label_glow, i, local.clone());
  127. // The vals expression is installed AND journaled in one
  128. // operation: unload unsets it, restoring the schema default.
  129. ctx.set_touched(
  130. atom,
  131. &prop,
  132. i,
  133. PropertyValue::SExpr(std::sync::Arc::new(expr::load_var(&local))),
  134. )?;
  135. }
  136. }
  137. // ------------------------------------------------------------------
  138. // 3. Factory-scrubbed one-offs restored as touched vals: the
  139. // "Copied link" overlay foreground (cyan in the shipped look) and
  140. // the text glows on the wallet amount/recipient values. Baked
  141. // vector-art shapes (netstatus icons, overlay panel, menu header,
  142. // editbox bg, wallet logos, emoji icon) keep their original
  143. // colors in the schema by design (non-goal: shape internals are
  144. // not tokenizable), so minimal renders them too.
  145. // ------------------------------------------------------------------
  146. if let Some(privmsg_node) =
  147. ctx.sg_root().lookup_node("/window/content/chat/main_chat_layer/content/chatty/privmsg")
  148. {
  149. if let Some(prop) = privmsg_node.get_property("url_copy_fg_color") {
  150. ctx.set_touched_color(atom, &prop, [0., 0.94, 1., 1.])?;
  151. }
  152. }
  153. // Text glows on amount/recipient values
  154. let glows = [
  155. (
  156. "/window/content/wallet/send_step4_layer/send_amount_wrapper4/send_amount_text4",
  157. [0., 0.9411, 1., 1.],
  158. AMOUNT_FONTSIZE / 1.6,
  159. ),
  160. (
  161. "/window/content/wallet/send_step4_layer/send_amount_wrapper4/send_amount_token_symbol4",
  162. [0., 0.9411, 1., 0.6],
  163. AMOUNT_FONTSIZE / 1.8,
  164. ),
  165. ("/window/content/wallet/tx_status_layer/tx_info/amount", [0.5451, 1., 1., 1.], BASE_FONTSIZE / 2.),
  166. (
  167. "/window/content/wallet/tx_status_layer/tx_info/recipient",
  168. [0., 1., 0.6392, 1.],
  169. BASE_FONTSIZE / 2.,
  170. ),
  171. ];
  172. for (path, color, blur) in glows {
  173. let node = ctx.sg_root().lookup_node(path).unwrap();
  174. let prop = node.get_property("text_shadow_color").unwrap();
  175. ctx.set_touched_color(atom, &prop, color)?;
  176. let prop = node.get_property("text_shadow_blur").unwrap();
  177. ctx.set_touched(atom, &prop, 0, PropertyValue::Float32(blur))?;
  178. }
  179. // Inset glow in the send button
  180. let node = ctx
  181. .sg_root()
  182. .lookup_node("/window/content/wallet/send_step4_layer/send_btn_layer/send_btn_shadow")
  183. .unwrap();
  184. let mut shape = VectorShape::new();
  185. shape.add_box_shadow_inset(
  186. expr::const_f32(0.),
  187. expr::const_f32(0.),
  188. expr::load_var("w"),
  189. expr::load_var("h"),
  190. 50.,
  191. [0., 1., 1., 0.25],
  192. );
  193. let prop = node.get_property("shape").unwrap();
  194. ctx.set_touched(atom, &prop, 0, PropertyValue::VectorShape(Arc::new(shape)))?;
  195. // Tx status glow
  196. let node = ctx.sg_root().lookup_node("/window/content/wallet/tx_status_layer").unwrap();
  197. let prop = node.get_property("glow_confirmed_color").unwrap();
  198. ctx.set_touched_color(atom, &prop, [0., 1., 0., 0.3])?;
  199. let prop = node.get_property("glow_error_color").unwrap();
  200. ctx.set_touched_color(atom, &prop, [1., 0., 0., 0.3])?;
  201. // ------------------------------------------------------------------
  202. // 4. King video background (tracked structural node, reserved low
  203. // z band under /window/content) plus the full-screen fade mesh
  204. // darkening it behind the chat screen. The wallet fade is a
  205. // schema-owned mesh on the wallet layer (schema/wallet/mod.rs):
  206. // theme-side one here would stack and double-darken.
  207. // ------------------------------------------------------------------
  208. king_video_node(ctx).await;
  209. bg_fade_node(ctx, "/window/content/chat/main_chat_layer", "bg").await?;
  210. // Bg for wallet btn to navigate to chat screen
  211. let node = create_vector_art("chat_btn_bg");
  212. let prop = node.get_property("rect").unwrap();
  213. let code = cc.compile(format!("w - {CHAT_BTN_SIZE} - {CHAT_BTN_MARGIN}")).unwrap();
  214. prop.set_default_expr(0, code).unwrap();
  215. let code = cc.compile(format!("h - {CHAT_BTN_SIZE} - {CHAT_BTN_MARGIN}")).unwrap();
  216. prop.set_default_expr(1, code).unwrap();
  217. prop.set_default_f32(2, CHAT_BTN_SIZE).unwrap();
  218. prop.set_default_f32(3, CHAT_BTN_SIZE).unwrap();
  219. node.set_property_u32(atom, Role::App, "z_index", 2).unwrap();
  220. let mut shape = VectorShape::new();
  221. shape.add_filled_box(
  222. expr::const_f32(0.),
  223. expr::const_f32(0.),
  224. expr::load_var("w"),
  225. expr::load_var("h"),
  226. [0., 0.098, 0.098, 1.],
  227. );
  228. shape.add_outline(
  229. expr::const_f32(0.),
  230. expr::const_f32(0.),
  231. expr::load_var("w"),
  232. expr::load_var("h"),
  233. 1.,
  234. [0.2, 0.2745, 0.2784, 1.],
  235. );
  236. node.set_property_shape(atom, Role::App, "shape", shape).unwrap();
  237. let node =
  238. node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
  239. let parent = ctx.sg_root().lookup_node("/window/content/wallet/main_layer").unwrap();
  240. ctx.link_tracked(&parent, node);
  241. // ------------------------------------------------------------------
  242. // 5. First-run scramble splash (theme content, design D12)
  243. // ------------------------------------------------------------------
  244. // ------------------------------------------------------------------
  245. // 6. Netstatus overlay fade: watch is_visible, animate alpha via
  246. // the ctx (touched-set dedups the per-step journal entries).
  247. // Replaces the in-handler fade from the reconnect click handler.
  248. // ------------------------------------------------------------------
  249. let overlay = ctx
  250. .sg_root()
  251. .lookup_node("/window/content/chat/netstatus_overlay")
  252. .ok_or(Error::NodeNotFound)?;
  253. let is_visible = overlay.get_property("is_visible").ok_or(Error::PropertyNotFound)?;
  254. let alpha = overlay.get_property("alpha").ok_or(Error::PropertyNotFound)?;
  255. let sub = is_visible.subscribe_modify();
  256. let redraw = ctx.redraw().clone();
  257. let ctx2 = ThemeCtx::clone_shallow(ctx);
  258. let fade_task = app.ex.spawn(async move {
  259. loop {
  260. let Ok((_, _, guard)) = sub.receive().await else { break };
  261. drop(guard);
  262. if !is_visible.get_bool(0).unwrap_or(false) {
  263. continue;
  264. }
  265. let steps = 50;
  266. // Start from fully transparent so the fade begins hidden
  267. {
  268. let atom0 = &mut redraw.make_guard(gfxtag!("netstatus overlay fade"));
  269. let _ = ctx2.set_touched(atom0, &alpha, 0, PropertyValue::Float32(0.));
  270. }
  271. for i in 1..=steps {
  272. msleep(1000 / steps as u64).await;
  273. let atom = &mut redraw.make_guard(gfxtag!("netstatus overlay fade"));
  274. if let Err(e) = ctx2.set_touched(
  275. atom,
  276. &alpha,
  277. 0,
  278. PropertyValue::Float32(i as f32 / steps as f32),
  279. ) {
  280. tracing::error!(target: "theme::scifi", "fade write failed: {e}");
  281. break;
  282. }
  283. }
  284. }
  285. });
  286. ctx.push_task(&token_child, fade_task);
  287. Ok(())
  288. }
  289. /// The king video background, as a tracked structural node.
  290. async fn king_video_node(ctx: &ThemeCtx) {
  291. let app = ctx.app().unwrap();
  292. let content = ctx.sg_root().lookup_node("/window/content").unwrap();
  293. let mut cc = Compiler::new();
  294. let node = create_video("king");
  295. let prop = node.get_property("rect").unwrap();
  296. prop.set_default_f32(0, 0.).unwrap();
  297. prop.set_default_f32(1, 0.).unwrap();
  298. prop.set_default_expr(2, expr::load_var("w")).unwrap();
  299. prop.set_default_expr(3, expr::load_var("h")).unwrap();
  300. cc.add_const_f32("R", VID_ASPECT_RATIO);
  301. let prop = node.get_property("uv").unwrap();
  302. #[rustfmt::skip]
  303. let code = cc.compile(indoc! {"
  304. r = w / h;
  305. if r < R { 0.5 - (r / (2 * R)) } else { 0 }
  306. "}).unwrap();
  307. prop.set_default_expr(0, code).unwrap();
  308. #[rustfmt::skip]
  309. let code = cc.compile(indoc! {"
  310. r = w / h;
  311. if r < R { 0 } else { 0.5 - (R / (2 * r)) }
  312. "}).unwrap();
  313. prop.set_default_expr(1, code).unwrap();
  314. #[rustfmt::skip]
  315. let code = cc.compile(indoc! {"
  316. r = w / h;
  317. if r < R { r / R } else { 1 }
  318. "}).unwrap();
  319. prop.set_default_expr(2, code).unwrap();
  320. #[rustfmt::skip]
  321. let code = cc.compile(indoc! {"
  322. r = w / h;
  323. if r < R { 1 } else { R / r }
  324. "}).unwrap();
  325. prop.set_default_expr(3, code).unwrap();
  326. node.set_property_str(&mut PropertyAtomicGuard::none(), Role::App, "path", VID_PATH).unwrap();
  327. node.set_property_u32(&mut PropertyAtomicGuard::none(), Role::App, "z_index", 0).unwrap();
  328. let node = node
  329. .setup(|me| {
  330. Video::new(me, app.renderer.clone(), app.redraw_trigger.clone(), app.ex.clone())
  331. })
  332. .await;
  333. ctx.link_tracked(&content, node);
  334. }
  335. /// The fade mesh below the shared header, darkening the king video behind a screen.
  336. async fn bg_fade_node(ctx: &ThemeCtx, parent_path: &str, name: &str) -> Result<()> {
  337. let Some(app) = ctx.app() else { return Err(Error::ThemeNotFound) };
  338. let parent = ctx.sg_root().lookup_node(parent_path).ok_or(Error::NodeNotFound)?;
  339. let node = create_vector_art(name);
  340. let prop = node.get_property("rect").unwrap();
  341. prop.set_default_f32(0, 0.).unwrap();
  342. prop.set_default_f32(1, MAIN_MENU_HEADER_HEIGHT + 1.).unwrap();
  343. prop.set_default_expr(2, expr::load_var("w")).unwrap();
  344. let cc = Compiler::new();
  345. prop.set_default_expr(3, cc.compile(format!("h - {MAIN_MENU_HEADER_HEIGHT} - 1")).unwrap())
  346. .unwrap();
  347. node.set_property_u32(&mut PropertyAtomicGuard::none(), Role::App, "z_index", 0).unwrap();
  348. let mut shape = VectorShape::new();
  349. shape.add_gradient_box(
  350. expr::const_f32(0.),
  351. expr::const_f32(0.),
  352. expr::load_var("w"),
  353. expr::load_var("h"),
  354. [[0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.5], [0., 0., 0., 0.8]],
  355. );
  356. node.set_property_shape(&mut PropertyAtomicGuard::none(), Role::App, "shape", shape).unwrap();
  357. let node =
  358. node.setup(|me| VectorArt::new(me, app.renderer.clone(), app.redraw_trigger.clone())).await;
  359. ctx.link_tracked(&parent, node);
  360. Ok(())
  361. }