text_scramble.rs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  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 async_trait::async_trait;
  19. use darkfi::system::msleep;
  20. use parking_lot::Mutex as SyncMutex;
  21. use rand::{rngs::OsRng, Rng};
  22. use std::{ops::Range, sync::Arc};
  23. use tracing::instrument;
  24. use crate::{
  25. gfx::{gfxtag, DrawCall, DrawInstruction, EpochCache, Rectangle, RenderApi, Renderer},
  26. mesh::MeshBuilder,
  27. prop::{
  28. PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyEnum, PropertyFloat32,
  29. PropertyRect, PropertyStr, PropertyUint32, Role,
  30. },
  31. scene::{Pimpl, SceneNodeWeak},
  32. text,
  33. util::i18n::I18nBabelFish,
  34. ExecutorPtr,
  35. };
  36. use super::{DrawUpdate, OnModify, RedrawTrigger, UIObject};
  37. pub type TextScramblePtr = Arc<TextScramble>;
  38. /// Glyphs shown for characters that have not locked in yet
  39. const SCRAMBLE_GLYPHS: &[char] = &[
  40. '!', '<', '>', '-', '_', '/', '\\', '[', ']', '{', '}', '(', ')', '=', '+', '*', '^', '?', '#',
  41. '$', '%', '&', '@', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  42. ];
  43. struct ScrambleState {
  44. /// Fully solved target text
  45. target: Vec<char>,
  46. /// Per-character solve flags, false means still scrambling
  47. solved: Vec<bool>,
  48. }
  49. impl ScrambleState {
  50. fn new(target: &str) -> Self {
  51. let target: Vec<char> = target.chars().collect();
  52. let solved = vec![false; target.len()];
  53. Self { target, solved }
  54. }
  55. fn reset(&mut self, target: &str) {
  56. self.target = target.chars().collect();
  57. self.solved = vec![false; self.target.len()];
  58. }
  59. fn solved_all(&self) -> bool {
  60. self.solved.iter().all(|&solved| solved)
  61. }
  62. /// Give every unsolved character a chance to lock in
  63. fn tick(&mut self, solve_probability: f32) {
  64. for solved in self.solved.iter_mut() {
  65. if *solved {
  66. continue
  67. }
  68. if OsRng.gen::<f32>() < solve_probability {
  69. *solved = true;
  70. }
  71. }
  72. }
  73. /// Current text with unsolved characters replaced by random glyphs,
  74. /// plus the byte ranges of those glyphs. Whitespace is kept so the
  75. /// word layout stays stable.
  76. fn display(&self) -> (String, Vec<Range<usize>>) {
  77. let mut text = String::new();
  78. let mut ranges = vec![];
  79. for (&c, &solved) in self.target.iter().zip(self.solved.iter()) {
  80. let start = text.len();
  81. if solved || c.is_whitespace() {
  82. text.push(c);
  83. } else {
  84. text.push(SCRAMBLE_GLYPHS[OsRng.gen_range(0..SCRAMBLE_GLYPHS.len())]);
  85. ranges.push(start..text.len());
  86. }
  87. }
  88. (text, ranges)
  89. }
  90. }
  91. pub struct TextScramble {
  92. node: SceneNodeWeak,
  93. renderer: Renderer,
  94. i18n_fish: I18nBabelFish,
  95. redraw: RedrawTrigger,
  96. tasks: SyncMutex<Vec<smol::Task<()>>>,
  97. dc_key: u64,
  98. rect: PropertyRect,
  99. height: PropertyFloat32,
  100. z_index: PropertyUint32,
  101. priority: PropertyUint32,
  102. text: PropertyStr,
  103. font_size: PropertyFloat32,
  104. text_color: PropertyColor,
  105. scramble_color: PropertyColor,
  106. lineheight: PropertyFloat32,
  107. text_align: PropertyEnum,
  108. overflow_wrap: PropertyEnum,
  109. use_i18n: PropertyBool,
  110. debug: PropertyBool,
  111. solve_probability: PropertyFloat32,
  112. tick_interval: PropertyUint32,
  113. window_scale: PropertyFloat32,
  114. /// Current scramble animation state
  115. scramble: SyncMutex<ScrambleState>,
  116. /// Cached layout + rendered instrs. Empty means stale: recompute in
  117. /// the draw pass. Layout is the expensive part (shaping, line breaks).
  118. /// Entries from a dead UI epoch are evicted automatically.
  119. draw_cache: EpochCache<(text::TextLayout, Vec<DrawInstruction>)>,
  120. }
  121. impl TextScramble {
  122. pub async fn new(
  123. node: SceneNodeWeak,
  124. window_scale: PropertyFloat32,
  125. renderer: Renderer,
  126. i18n_fish: I18nBabelFish,
  127. redraw: RedrawTrigger,
  128. ) -> Pimpl {
  129. let node_ref = &node.upgrade().unwrap();
  130. let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
  131. let height = PropertyFloat32::wrap(node_ref, Role::Internal, "height", 0).unwrap();
  132. let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
  133. let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
  134. let text = PropertyStr::wrap(node_ref, Role::Internal, "text", 0).unwrap();
  135. let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
  136. let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
  137. let scramble_color =
  138. PropertyColor::wrap(node_ref, Role::Internal, "scramble_color").unwrap();
  139. let lineheight = PropertyFloat32::wrap(node_ref, Role::Internal, "lineheight", 0).unwrap();
  140. let text_align = PropertyEnum::wrap(node_ref, Role::Internal, "text_align", 0).unwrap();
  141. let overflow_wrap =
  142. PropertyEnum::wrap(node_ref, Role::Internal, "overflow_wrap", 0).unwrap();
  143. let use_i18n = PropertyBool::wrap(node_ref, Role::Internal, "use_i18n", 0).unwrap();
  144. let debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap();
  145. let solve_probability =
  146. PropertyFloat32::wrap(node_ref, Role::Internal, "solve_probability", 0).unwrap();
  147. let tick_interval =
  148. PropertyUint32::wrap(node_ref, Role::Internal, "tick_interval", 0).unwrap();
  149. let draw_cache = EpochCache::new(&renderer);
  150. let target = Self::translated_target(&text.get(), &use_i18n, &i18n_fish);
  151. let scramble = SyncMutex::new(ScrambleState::new(&target));
  152. let self_ = Arc::new(Self {
  153. node,
  154. renderer,
  155. i18n_fish,
  156. redraw,
  157. tasks: SyncMutex::new(vec![]),
  158. dc_key: OsRng.gen(),
  159. rect,
  160. height,
  161. z_index,
  162. priority,
  163. text,
  164. font_size,
  165. text_color,
  166. scramble_color,
  167. lineheight,
  168. text_align,
  169. overflow_wrap,
  170. use_i18n,
  171. debug,
  172. solve_probability,
  173. tick_interval,
  174. window_scale,
  175. scramble,
  176. draw_cache,
  177. });
  178. Pimpl::TextScramble(self_)
  179. }
  180. fn translated_target(text: &str, use_i18n: &PropertyBool, i18n_fish: &I18nBabelFish) -> String {
  181. if use_i18n.get() {
  182. if let Some(trans) = i18n_fish.tr(text) {
  183. return trans
  184. }
  185. return format!("tr err: {}", text)
  186. }
  187. text.to_string()
  188. }
  189. /// Throw away the current solve state and rescramble from the target text
  190. fn reset_scramble(&self) {
  191. let target = Self::translated_target(&self.text.get(), &self.use_i18n, &self.i18n_fish);
  192. self.scramble.lock().reset(&target);
  193. }
  194. fn make_layout(&self) -> text::TextLayout {
  195. let (text, scramble_ranges) = self.scramble.lock().display();
  196. let font_size = self.font_size.get();
  197. let lineheight = self.lineheight.get();
  198. let text_color = self.text_color.get();
  199. let scramble_color = self.scramble_color.get();
  200. let window_scale = self.window_scale.get();
  201. let width = self.rect.get_width();
  202. let text_align = match self.text_align.get().as_str() {
  203. "end" => parley::Alignment::End,
  204. "left" => parley::Alignment::Left,
  205. "center" => parley::Alignment::Center,
  206. "right" => parley::Alignment::Right,
  207. "justify" => parley::Alignment::Justify,
  208. _ => parley::Alignment::Start,
  209. };
  210. let overflow_wrap = match self.overflow_wrap.get().as_str() {
  211. "anywhere" => parley::OverflowWrap::Anywhere,
  212. "break-word" => parley::OverflowWrap::BreakWord,
  213. _ => parley::OverflowWrap::Normal,
  214. };
  215. let scramble_colors =
  216. scramble_ranges.into_iter().map(|range| (range, scramble_color)).collect::<Vec<_>>();
  217. text::make_layout2(
  218. &text,
  219. text_color,
  220. font_size,
  221. lineheight,
  222. window_scale,
  223. Some(width),
  224. &[],
  225. &scramble_colors,
  226. text_align,
  227. overflow_wrap,
  228. )
  229. }
  230. fn regen_mesh(&self, layout: &text::TextLayout) -> Vec<DrawInstruction> {
  231. let mut debug_opts = text::DebugRenderOptions::OFF;
  232. if self.debug.get() {
  233. debug_opts |= text::DebugRenderOptions::BASELINE;
  234. }
  235. text::render_layout_with_opts(layout, debug_opts, &self.renderer, gfxtag!("text_scramble"))
  236. }
  237. fn get_draw_calls(
  238. &self,
  239. atom: &mut PropertyAtomicGuard,
  240. parent_rect: Rectangle,
  241. ) -> Option<DrawUpdate> {
  242. // Rect property is its own memo: compare before/after eval.
  243. let prev_rect = self.rect.get();
  244. self.rect.eval(atom, &parent_rect).ok()?;
  245. let rect = self.rect.get();
  246. let rect_changed = rect != prev_rect;
  247. // Layout depends on the width, so a rect change invalidates the
  248. // layout even if the text itself did not change. Compute under the
  249. // cache lock: the compute is synchronous, so concurrent invalidations
  250. // either land before (seen as None) or after (they clear our result).
  251. if rect_changed {
  252. self.draw_cache.clear();
  253. }
  254. let (layout, mut instrs) = self.draw_cache.get_or_insert_with(|| {
  255. let layout = self.make_layout();
  256. let mut instrs = vec![DrawInstruction::Move(rect.pos())];
  257. instrs.append(&mut self.regen_mesh(&layout));
  258. (layout, instrs)
  259. });
  260. // Height output for parents that depend on it.
  261. self.height.set(atom, layout.height());
  262. if self.debug.get() {
  263. let rect = self.rect.get().with_zero_pos();
  264. let mut mesh = MeshBuilder::new(gfxtag!("text_scramble_debug-rect"));
  265. mesh.draw_outline(&rect, [0., 1., 0., 0.7], 1.);
  266. let mesh = mesh.alloc(&self.renderer).draw_untextured();
  267. instrs.push(DrawInstruction::Draw(mesh));
  268. }
  269. Some(DrawUpdate {
  270. key: self.dc_key,
  271. draw_calls: vec![(
  272. self.dc_key,
  273. DrawCall::new(instrs, vec![], self.z_index.get(), "text_scramble"),
  274. )],
  275. })
  276. }
  277. }
  278. #[async_trait]
  279. impl UIObject for TextScramble {
  280. fn priority(&self) -> u32 {
  281. self.priority.get()
  282. }
  283. async fn start(self: Arc<Self>, ex: ExecutorPtr) {
  284. let me = Arc::downgrade(&self);
  285. let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
  286. // Invalidate the cache, then request a pass. Internal-role echoes
  287. // (the pass's own evals) are skipped.
  288. on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
  289. self_.draw_cache.clear();
  290. self_.redraw.trigger();
  291. });
  292. on_modify.when_change_external(self.z_index.prop(), |self_, _| async move {
  293. self_.draw_cache.clear();
  294. self_.redraw.trigger();
  295. });
  296. on_modify.when_change_external(self.text.prop(), |self_, _| async move {
  297. self_.reset_scramble();
  298. self_.draw_cache.clear();
  299. self_.redraw.trigger();
  300. });
  301. on_modify.when_change_external(self.text_align.prop(), |self_, _| async move {
  302. self_.draw_cache.clear();
  303. self_.redraw.trigger();
  304. });
  305. on_modify.when_change_external(self.font_size.prop(), |self_, _| async move {
  306. self_.draw_cache.clear();
  307. self_.redraw.trigger();
  308. });
  309. on_modify.when_change_external(self.text_color.prop(), |self_, _| async move {
  310. self_.draw_cache.clear();
  311. self_.redraw.trigger();
  312. });
  313. on_modify.when_change_external(self.scramble_color.prop(), |self_, _| async move {
  314. self_.draw_cache.clear();
  315. self_.redraw.trigger();
  316. });
  317. on_modify.when_change_external(self.debug.prop(), |self_, _| async move {
  318. self_.draw_cache.clear();
  319. self_.redraw.trigger();
  320. });
  321. *self.tasks.lock() = on_modify.tasks;
  322. // Scramble animation loop. Weak ref: the task must not keep the
  323. // node alive, and self_ is dropped before each sleep so a dead
  324. // node ends the loop on the next upgrade.
  325. let anim_task = ex.spawn(async move {
  326. loop {
  327. let delay = {
  328. let Some(self_) = me.upgrade() else { break };
  329. let interval = self_.tick_interval.get().max(1);
  330. let probability = self_.solve_probability.get();
  331. let running = {
  332. let mut scramble = self_.scramble.lock();
  333. let running = !scramble.solved_all();
  334. if running {
  335. scramble.tick(probability);
  336. }
  337. running
  338. };
  339. if running {
  340. self_.draw_cache.clear();
  341. self_.redraw.trigger();
  342. }
  343. interval as u64
  344. };
  345. msleep(delay).await;
  346. }
  347. });
  348. self.tasks.lock().push(anim_task);
  349. }
  350. fn stop(&self) {
  351. self.tasks.lock().clear();
  352. self.draw_cache.clear();
  353. }
  354. #[instrument(target = "ui::text_scramble")]
  355. async fn draw(
  356. &self,
  357. parent_rect: Rectangle,
  358. atom: &mut PropertyAtomicGuard,
  359. ) -> Option<DrawUpdate> {
  360. self.get_draw_calls(atom, parent_rect)
  361. }
  362. fn set_i18n(&self, i18n_fish: &I18nBabelFish) {
  363. self.i18n_fish.set(i18n_fish);
  364. }
  365. }
  366. impl Drop for TextScramble {
  367. fn drop(&mut self) {
  368. self.renderer.replace_draw_calls(vec![(self.dc_key, Default::default())]);
  369. }
  370. }
  371. impl std::fmt::Debug for TextScramble {
  372. fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
  373. write!(f, "{:?}", self.node.upgrade().unwrap())
  374. }
  375. }