mod.rs 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  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 super::long_press_timeout;
  19. use async_trait::async_trait;
  20. use atomic_float::AtomicF32;
  21. use darkfi::system::CondVar;
  22. use darkfi_serial::{serialize, Decodable};
  23. use miniquad::MouseButton;
  24. use parking_lot::Mutex as SyncMutex;
  25. use rand::{rngs::OsRng, Rng};
  26. use std::{
  27. collections::HashSet,
  28. io::Read,
  29. sync::{
  30. atomic::{AtomicBool, Ordering},
  31. Arc, Weak,
  32. },
  33. };
  34. use crate::{
  35. gfx::{gfxtag, DrawCall, DrawInstruction, DrawMesh, Point, Rectangle, Renderer, Vertex},
  36. mesh::MeshBuilder,
  37. prop::{
  38. PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr,
  39. PropertyRect, PropertyUint32, Role,
  40. },
  41. scene::{MethodCallSub, Pimpl, SceneNodeWeak},
  42. text, ExecutorPtr,
  43. };
  44. use super::{DrawUpdate, GestureAction, GestureSet, OnModify, RedrawTrigger, UIObject};
  45. mod shape;
  46. const EPSILON: f32 = 0.001;
  47. const BIG_EPSILON: f32 = 0.05;
  48. const LONG_PRESS_EPSILON: f32 = 5.0;
  49. #[cfg(target_os = "android")]
  50. const MENU_ICON_OFFSET: f32 = 55.;
  51. #[cfg(not(target_os = "android"))]
  52. const MENU_ICON_OFFSET: f32 = 24.;
  53. macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::menu", $($arg)*); } }
  54. macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::menu", $($arg)*); } }
  55. #[derive(Clone)]
  56. struct MouseClickInfo {
  57. start_pos: Point,
  58. start_instant: std::time::Instant,
  59. }
  60. #[derive(Clone)]
  61. struct DragInfo {
  62. item_idx: usize,
  63. insert_idx: usize,
  64. }
  65. pub type MenuPtr = Arc<Menu>;
  66. pub struct Menu {
  67. node: SceneNodeWeak,
  68. /// Weak self-reference so handlers can spawn detached tasks.
  69. me: Weak<Self>,
  70. ex: ExecutorPtr,
  71. renderer: Renderer,
  72. redraw: RedrawTrigger,
  73. tasks: SyncMutex<Vec<smol::Task<()>>>,
  74. root_dc_key: u64,
  75. content_dc_key: u64,
  76. bg_dc_key: u64,
  77. is_visible: PropertyBool,
  78. rect: PropertyRect,
  79. scroll: AtomicF32,
  80. z_index: PropertyUint32,
  81. priority: PropertyUint32,
  82. items: PropertyPtr,
  83. font_size: PropertyFloat32,
  84. padding: PropertyPtr,
  85. handle_padding: PropertyFloat32,
  86. text_color: PropertyColor,
  87. bg_color: PropertyColor,
  88. sep_size: PropertyFloat32,
  89. sep_color: PropertyColor,
  90. role1_color: PropertyColor,
  91. role1_group: PropertyPtr,
  92. role2_color: PropertyColor,
  93. role2_group: PropertyPtr,
  94. fade_zone: PropertyFloat32,
  95. window_scale: PropertyFloat32,
  96. mouse_pos: SyncMutex<Point>,
  97. mouse_click_info: SyncMutex<Option<MouseClickInfo>>,
  98. drag_info: SyncMutex<Option<DragInfo>>,
  99. long_press_task: SyncMutex<Option<smol::Task<()>>>,
  100. /// Active 1:1 scroll drag: (finger y at drag start, scroll at drag
  101. /// start), parent space.
  102. drag_state: SyncMutex<Option<(f32, f32)>>,
  103. scroll_start_accel: PropertyFloat32,
  104. scroll_resist: PropertyFloat32,
  105. overscroll: PropertyFloat32,
  106. motion_cv: Arc<CondVar>,
  107. speed: AtomicF32,
  108. is_edit_mode: AtomicBool,
  109. parent_rect: SyncMutex<Option<Rectangle>>,
  110. saved_items: SyncMutex<Option<Vec<String>>>,
  111. /// Opaque per-item text instructions, indexed by item position.
  112. item_instrs: SyncMutex<Vec<Option<Vec<DrawInstruction>>>>,
  113. /// Content instructions tagged with the scroll offset they were
  114. /// assembled for. `None` means stale.
  115. content_cache: SyncMutex<Option<(f32, Vec<DrawInstruction>)>>,
  116. /// Viewport-anchored background meshes: opaque part plus fade
  117. /// gradient. `None` means stale.
  118. bg_meshes: SyncMutex<Option<(DrawMesh, Option<DrawMesh>)>>,
  119. }
  120. impl Menu {
  121. pub async fn new(
  122. node: SceneNodeWeak,
  123. window_scale: PropertyFloat32,
  124. renderer: Renderer,
  125. redraw: RedrawTrigger,
  126. ex: ExecutorPtr,
  127. ) -> Pimpl {
  128. let node_ref = &node.upgrade().unwrap();
  129. let is_visible = PropertyBool::wrap(node_ref, Role::Internal, "is_visible", 0).unwrap();
  130. let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
  131. let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
  132. let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
  133. let items = node_ref.get_property("items").expect("Menu::items");
  134. let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
  135. let padding = node_ref.get_property("padding").expect("Menu::padding");
  136. let handle_padding =
  137. PropertyFloat32::wrap(node_ref, Role::Internal, "handle_padding", 0).unwrap();
  138. let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
  139. let bg_color = PropertyColor::wrap(node_ref, Role::Internal, "bg_color").unwrap();
  140. let sep_size = PropertyFloat32::wrap(node_ref, Role::Internal, "sep_size", 0).unwrap();
  141. let sep_color = PropertyColor::wrap(node_ref, Role::Internal, "sep_color").unwrap();
  142. let role1_color = PropertyColor::wrap(node_ref, Role::Internal, "role1_color").unwrap();
  143. let role1_group = node_ref.get_property("role1_group").unwrap();
  144. let role2_color = PropertyColor::wrap(node_ref, Role::Internal, "role2_color").unwrap();
  145. let role2_group = node_ref.get_property("role2_group").unwrap();
  146. let fade_zone = PropertyFloat32::wrap(node_ref, Role::Internal, "fade_zone", 0).unwrap();
  147. let scroll_start_accel =
  148. PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_start_accel", 0).unwrap();
  149. let scroll_resist =
  150. PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_resist", 0).unwrap();
  151. let overscroll = PropertyFloat32::wrap(node_ref, Role::Internal, "overscroll", 0).unwrap();
  152. let motion_cv = Arc::new(CondVar::new());
  153. let self_ = Arc::new_cyclic(|me| Self {
  154. node: node.clone(),
  155. me: me.clone(),
  156. ex,
  157. renderer: renderer.clone(),
  158. redraw,
  159. tasks: SyncMutex::new(vec![]),
  160. root_dc_key: OsRng.gen(),
  161. content_dc_key: OsRng.gen(),
  162. bg_dc_key: OsRng.gen(),
  163. is_visible,
  164. rect,
  165. scroll: AtomicF32::new(0.),
  166. z_index,
  167. priority,
  168. items,
  169. font_size,
  170. padding,
  171. handle_padding,
  172. text_color,
  173. bg_color,
  174. sep_size,
  175. sep_color,
  176. role1_color,
  177. role1_group,
  178. role2_color,
  179. role2_group,
  180. fade_zone,
  181. window_scale,
  182. mouse_pos: SyncMutex::new(Point::new(0., 0.)),
  183. mouse_click_info: SyncMutex::new(None),
  184. drag_info: SyncMutex::new(None),
  185. long_press_task: SyncMutex::new(None),
  186. drag_state: SyncMutex::new(None),
  187. scroll_start_accel,
  188. scroll_resist,
  189. overscroll,
  190. motion_cv,
  191. speed: AtomicF32::new(0.),
  192. is_edit_mode: AtomicBool::new(false),
  193. parent_rect: SyncMutex::new(None),
  194. saved_items: SyncMutex::new(None),
  195. item_instrs: SyncMutex::new(vec![]),
  196. content_cache: SyncMutex::new(None),
  197. bg_meshes: SyncMutex::new(None),
  198. });
  199. Pimpl::Menu(self_)
  200. }
  201. /// Invalidate all cached draw artifacts. Locks are taken one at a
  202. /// time, never nested, so this cannot deadlock against `draw()`.
  203. fn invalidate_draw(&self) {
  204. *self.item_instrs.lock() = vec![];
  205. *self.content_cache.lock() = None;
  206. *self.bg_meshes.lock() = None;
  207. }
  208. /// Height of a single item
  209. fn get_item_height(&self) -> f32 {
  210. self.font_size.get() + self.padding.get_f32(1).unwrap() * 2.0
  211. }
  212. /// Save the current menu items layout
  213. fn save_items_layout(&self) {
  214. let items = self.items.get_str_vec().unwrap();
  215. *self.saved_items.lock() = Some(items);
  216. }
  217. /// Height of the content without the overscroll
  218. fn content_height(&self) -> f32 {
  219. self.items.get_len() as f32 * self.get_item_height()
  220. }
  221. fn get_selected_item_index(&self, click_y: f32) -> Option<usize> {
  222. let rect = self.rect.get();
  223. let scroll = self.scroll.load(Ordering::Relaxed);
  224. // Scroll is positive value so to translate click into content, we must add the scroll.
  225. let content_y = click_y + scroll - rect.y;
  226. if content_y < 0. || content_y > self.content_height() {
  227. return None
  228. }
  229. let item_height = self.get_item_height();
  230. Some((content_y / item_height) as usize)
  231. }
  232. async fn handle_selection(&self, item_idx: usize) {
  233. if item_idx < self.items.get_len() {
  234. let node = self.node.upgrade().unwrap();
  235. let item_name = self.items.get_str(item_idx).unwrap();
  236. let data = serialize(&item_name);
  237. node.trigger("select", data).await.unwrap();
  238. }
  239. }
  240. async fn handle_interaction(
  241. &self,
  242. pos: Point,
  243. is_tap: bool,
  244. is_long_press_tap: bool,
  245. elapsed_ms: u128,
  246. ) {
  247. let is_long_press = is_long_press_tap && elapsed_ms >= long_press_timeout() as u128;
  248. if is_long_press {
  249. if !self.is_edit_mode.load(Ordering::Relaxed) {
  250. self.save_items_layout();
  251. self.is_edit_mode.store(true, Ordering::Release);
  252. let node = self.node.upgrade().unwrap();
  253. node.trigger("edit_active", vec![]).await.unwrap();
  254. self.invalidate_draw();
  255. self.redraw.trigger();
  256. }
  257. } else if is_tap {
  258. let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
  259. if is_edit_mode {
  260. let font_size = self.font_size.get();
  261. let x_half_size = font_size * 0.85;
  262. let x_center = MENU_ICON_OFFSET + font_size * 0.4;
  263. if let Some(item_idx) = self.get_selected_item_index(pos.y) {
  264. let item_name = self.items.get_str(item_idx).unwrap();
  265. let x_min = x_center - x_half_size;
  266. let x_max = x_center + x_half_size;
  267. if pos.x >= x_min && pos.x <= x_max {
  268. info!(target: "app::menu", "X clicked for item: {item_name}");
  269. let atom = &mut self.redraw.make_guard(gfxtag!("Menu::delete_item"));
  270. self.items.remove_str(atom, Role::App, item_idx).unwrap();
  271. } else {
  272. self.handle_selection(item_idx).await;
  273. }
  274. }
  275. } else if let Some(item_idx) = self.get_selected_item_index(pos.y) {
  276. self.handle_selection(item_idx).await;
  277. }
  278. }
  279. }
  280. /// Fade alpha for a content-space y position at the given scroll:
  281. /// 1 above the fade zone, decreasing linearly to 0 at the bottom
  282. /// edge of the viewport.
  283. fn fade_factor(&self, rect: &Rectangle, content_y: f32, scroll: f32) -> f32 {
  284. let fade_distance = self.fade_zone.get();
  285. if fade_distance <= EPSILON {
  286. return 1.0
  287. }
  288. let viewport_y = content_y - scroll;
  289. let fade_zone_start = rect.h - fade_distance;
  290. if viewport_y <= fade_zone_start {
  291. return 1.0
  292. }
  293. 1.0 - ((viewport_y - fade_zone_start) / fade_distance).clamp(0.0, 1.0)
  294. }
  295. /// Render a single item's text as draw instructions with the given
  296. /// color.
  297. fn make_text_instrs(
  298. &self,
  299. item_text: &str,
  300. color: crate::mesh::Color,
  301. rect: &Rectangle,
  302. font_size: f32,
  303. window_scale: f32,
  304. padding_x: f32,
  305. ) -> Vec<DrawInstruction> {
  306. let layout = text::make_layout(
  307. item_text,
  308. color,
  309. font_size,
  310. 1.0,
  311. window_scale,
  312. Some(rect.w - padding_x * 2.),
  313. &[],
  314. );
  315. text::render_layout(&layout, &self.renderer, gfxtag!("menu_text"))
  316. }
  317. /// Assemble the content draw instructions for the given scroll
  318. /// offset. Items away from the fade zone reuse cached opaque text
  319. /// instructions; items intersecting it are rebuilt with faded
  320. /// colors. Separators are baked into one mesh with per-separator
  321. /// alpha at absolute content positions.
  322. fn assemble_content(&self, rect: &Rectangle, scroll: f32) -> Vec<DrawInstruction> {
  323. let mut instrs = vec![];
  324. let item_height = self.get_item_height();
  325. let font_size = self.font_size.get();
  326. let padding_x = self.padding.get_f32(0).unwrap();
  327. let padding_y = self.padding.get_f32(1).unwrap();
  328. let handle_padding = self.handle_padding.get();
  329. let text_color = self.text_color.get();
  330. let role1_color = self.role1_color.get();
  331. let role2_color = self.role2_color.get();
  332. let sep_size = self.sep_size.get();
  333. let sep_color = self.sep_color.get();
  334. let window_scale = self.window_scale.get();
  335. let role1_set: HashSet<String> =
  336. self.role1_group.get_str_vec().unwrap_or_default().into_iter().collect();
  337. let role2_set: HashSet<String> =
  338. self.role2_group.get_str_vec().unwrap_or_default().into_iter().collect();
  339. let num_items = self.items.get_len();
  340. // Get items and reorder if dragging
  341. let items_list = {
  342. let mut items = vec![];
  343. for idx in 0..num_items {
  344. items.push(self.items.get_str(idx).unwrap());
  345. }
  346. if let Some(ref drag_info) = self.drag_info.lock().as_ref() {
  347. if drag_info.item_idx != drag_info.insert_idx {
  348. let item = items.remove(drag_info.item_idx);
  349. items.insert(drag_info.insert_idx, item);
  350. }
  351. }
  352. items
  353. };
  354. // Single separator mesh covering all separators, each faded by
  355. // its on-screen position. Drawn first while the cursor sits at
  356. // the content origin, so baked positions map directly.
  357. if num_items > 1 {
  358. let mut sep_builder = MeshBuilder::new(gfxtag!("menu_sep"));
  359. let uv = [0., 0.];
  360. for idx in 0..num_items - 1 {
  361. let y = (idx + 1) as f32 * item_height;
  362. let factor = self.fade_factor(rect, y, scroll);
  363. if factor <= 0.0 {
  364. continue
  365. }
  366. let color = [sep_color[0], sep_color[1], sep_color[2], sep_color[3] * factor];
  367. sep_builder.append(
  368. vec![
  369. Vertex { pos: [0., y], color, uv },
  370. Vertex { pos: [rect.w, y], color, uv },
  371. Vertex { pos: [0., y + sep_size], color, uv },
  372. Vertex { pos: [rect.w, y + sep_size], color, uv },
  373. ],
  374. vec![0, 2, 1, 1, 2, 3],
  375. );
  376. }
  377. let sep_mesh = sep_builder.alloc(&self.renderer).draw_untextured();
  378. instrs.push(DrawInstruction::Draw(sep_mesh));
  379. }
  380. let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
  381. let edit_offset = if is_edit_mode { handle_padding } else { 0.0 };
  382. let mut edit_instrs = vec![];
  383. if is_edit_mode {
  384. let item_center_y = item_height / 2.0;
  385. let x_center = MENU_ICON_OFFSET + font_size * 0.4;
  386. let rhs = rect.w - MENU_ICON_OFFSET - font_size * 0.56;
  387. edit_instrs.push(DrawInstruction::Move(Point::new(x_center, item_center_y)));
  388. edit_instrs.push(DrawInstruction::Draw(shape::make_x(&self.renderer, font_size)));
  389. edit_instrs.push(DrawInstruction::Move(Point::new(-x_center, -item_center_y)));
  390. edit_instrs.push(DrawInstruction::Move(Point::new(rhs, item_center_y)));
  391. edit_instrs.push(DrawInstruction::Draw(shape::make_hammy(&self.renderer, font_size)));
  392. edit_instrs.push(DrawInstruction::Move(Point::new(-rhs, -item_center_y)));
  393. }
  394. let mut item_instrs = self.item_instrs.lock();
  395. if item_instrs.len() != num_items {
  396. item_instrs.clear();
  397. item_instrs.resize(num_items, None);
  398. }
  399. for idx in 0..num_items {
  400. let item_text = items_list[idx].clone();
  401. let base_color = if role2_set.contains(&item_text) {
  402. role2_color
  403. } else if role1_set.contains(&item_text) {
  404. role1_color
  405. } else {
  406. text_color
  407. };
  408. let factor = self.fade_factor(rect, idx as f32 * item_height, scroll);
  409. if factor <= 0.0 {
  410. continue
  411. }
  412. instrs.append(&mut edit_instrs.clone());
  413. // Use a fraction of edit_offset for the label position to reduce gap from X icon
  414. let label_edit_offset = edit_offset * 0.62;
  415. instrs
  416. .push(DrawInstruction::Move(Point::new(padding_x + label_edit_offset, padding_y)));
  417. if factor >= 1.0 {
  418. if item_instrs[idx].is_none() {
  419. item_instrs[idx] = Some(self.make_text_instrs(
  420. &item_text,
  421. base_color,
  422. rect,
  423. font_size,
  424. window_scale,
  425. padding_x,
  426. ));
  427. }
  428. instrs.extend(item_instrs[idx].clone().unwrap());
  429. } else {
  430. let mut faded = base_color;
  431. faded[3] *= factor;
  432. instrs.extend(self.make_text_instrs(
  433. &item_text,
  434. faded,
  435. rect,
  436. font_size,
  437. window_scale,
  438. padding_x,
  439. ));
  440. }
  441. instrs.push(DrawInstruction::Move(Point::new(
  442. -padding_x - label_edit_offset,
  443. font_size + padding_y,
  444. )));
  445. }
  446. instrs
  447. }
  448. /// Build the viewport-anchored background meshes: an opaque quad
  449. /// above the fade zone and a gradient quad fading to fully
  450. /// transparent at the bottom edge, so the parent background shows
  451. /// through.
  452. fn make_bg_meshes(&self, rect: &Rectangle) -> (DrawMesh, Option<DrawMesh>) {
  453. let fade_distance = self.fade_zone.get();
  454. let content_height = self.content_height();
  455. let bg_color = self.bg_color.get();
  456. let fade_top = if fade_distance > EPSILON { rect.h - fade_distance } else { rect.h };
  457. let main_bottom = fade_top.min(content_height).min(rect.h);
  458. let mut main_builder = MeshBuilder::new(gfxtag!("menu_bg"));
  459. if main_bottom > 0. {
  460. main_builder.draw_filled_box(&Rectangle::new(0., 0., rect.w, main_bottom), bg_color);
  461. }
  462. let main_mesh = main_builder.alloc(&self.renderer).draw_untextured();
  463. let fade_mesh = if fade_distance > EPSILON && content_height > fade_top {
  464. let fade_bottom = rect.h.min(content_height);
  465. Some(shape::make_fade_mesh(&self.renderer, rect.w, fade_top, fade_bottom, bg_color))
  466. } else {
  467. None
  468. };
  469. (main_mesh, fade_mesh)
  470. }
  471. fn scrollview(&self, scroll: f32) {
  472. let item_height = self.get_item_height();
  473. let num_items = self.items.get_len() as f32;
  474. let content_height = num_items * item_height;
  475. let rect = self.rect.get();
  476. let overscroll = self.overscroll.get();
  477. let max_scroll = (overscroll + content_height - rect.h).max(0.);
  478. let scroll = scroll.clamp(0., max_scroll);
  479. self.scroll.store(scroll, Ordering::Relaxed);
  480. }
  481. fn start_scroll(&self, delta: f32) {
  482. let accel = self.scroll_start_accel.get();
  483. self.speed.store(delta * accel, Ordering::Relaxed);
  484. self.motion_cv.notify();
  485. }
  486. async fn handle_movement(&self) {
  487. let resist = self.scroll_resist.get();
  488. loop {
  489. let mut speed = self.speed.load(Ordering::Relaxed);
  490. if speed.abs() < EPSILON {
  491. break
  492. }
  493. while speed.abs() >= EPSILON {
  494. speed = self.speed.load(Ordering::Relaxed);
  495. let scroll = self.scroll.load(Ordering::Relaxed);
  496. self.scrollview(scroll + speed);
  497. self.redraw.trigger();
  498. speed *= resist;
  499. self.speed.store(speed, Ordering::Relaxed);
  500. darkfi::system::msleep(16).await;
  501. }
  502. self.speed.store(0., Ordering::Relaxed);
  503. break
  504. }
  505. }
  506. /// Cancels edit mode changes, reverting any modifications made during edit mode
  507. async fn process_cancel_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
  508. let Ok(method_call) = sub.receive().await else {
  509. d!("Event relayer closed");
  510. return false
  511. };
  512. d!("method called: cancel({method_call:?})");
  513. assert!(method_call.send_res.is_none());
  514. let Some(self_) = me.upgrade() else {
  515. d!("Self destroyed");
  516. return true
  517. };
  518. let atom = &mut self_.redraw.make_guard(gfxtag!("Menu::cancel_edit"));
  519. // Restore the saved items
  520. // It must exist otherwise theres a logic err
  521. let saved_items = self_.saved_items.lock().take().unwrap();
  522. self_.items.set_str_vec(atom, Role::App, saved_items).unwrap();
  523. // Exit edit mode
  524. self_.is_edit_mode.store(false, Ordering::Release);
  525. self_.invalidate_draw();
  526. true
  527. }
  528. /// Accepts edit mode changes, finalizing any modifications made during edit mode
  529. async fn process_done_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
  530. let Ok(method_call) = sub.receive().await else {
  531. d!("Event relayer closed");
  532. return false
  533. };
  534. d!("method called: done({method_call:?})");
  535. assert!(method_call.send_res.is_none());
  536. let Some(self_) = me.upgrade() else {
  537. d!("Self destroyed");
  538. return true
  539. };
  540. // Calculate deleted items by diffing saved and current items
  541. let saved_items = self_.saved_items.lock().take().unwrap();
  542. let current_items = self_.items.get_str_vec().unwrap();
  543. let deleted_items: Vec<String> =
  544. saved_items.into_iter().filter(|item| !current_items.contains(item)).collect();
  545. // Send the edit_done signal with deleted items
  546. let node = self_.node.upgrade().unwrap();
  547. let data = serialize(&deleted_items);
  548. node.trigger("edit_done", data).await.unwrap();
  549. self_.is_edit_mode.store(false, Ordering::Release);
  550. self_.invalidate_draw();
  551. self_.redraw.trigger();
  552. true
  553. }
  554. }
  555. #[async_trait]
  556. impl UIObject for Menu {
  557. fn priority(&self) -> u32 {
  558. self.priority.get()
  559. }
  560. async fn start(self: Arc<Self>, ex: ExecutorPtr) {
  561. let me = self.me.clone();
  562. let node_ref = &self.node.upgrade().unwrap();
  563. let me2 = me.clone();
  564. let cv = self.motion_cv.clone();
  565. let motion_task = ex.spawn(async move {
  566. loop {
  567. cv.wait().await;
  568. let Some(self_) = me2.upgrade() else {
  569. t!("Menu destroyed before motion_task stopped");
  570. break;
  571. };
  572. self_.handle_movement().await;
  573. cv.reset();
  574. }
  575. });
  576. let method_sub = node_ref.subscribe_method_call("cancel_edit").unwrap();
  577. let me2 = me.clone();
  578. let cancel_task =
  579. ex.spawn(async move { while Self::process_cancel_method(&me2, &method_sub).await {} });
  580. let method_sub = node_ref.subscribe_method_call("done_edit").unwrap();
  581. let me2 = me.clone();
  582. let done_task =
  583. ex.spawn(async move { while Self::process_done_method(&me2, &method_sub).await {} });
  584. let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
  585. on_modify.when_change_external(self.items.clone(), |self_, _| async move {
  586. self_.invalidate_draw();
  587. self_.redraw.trigger();
  588. });
  589. on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
  590. self_.invalidate_draw();
  591. self_.redraw.trigger();
  592. });
  593. on_modify.when_change_external(self.font_size.prop(), |self_, _| async move {
  594. self_.invalidate_draw();
  595. self_.redraw.trigger();
  596. });
  597. on_modify.when_change_external(self.padding.clone(), |self_, _| async move {
  598. self_.invalidate_draw();
  599. self_.redraw.trigger();
  600. });
  601. on_modify.when_change_external(self.text_color.prop(), |self_, _| async move {
  602. self_.invalidate_draw();
  603. self_.redraw.trigger();
  604. });
  605. on_modify.when_change_external(self.bg_color.prop(), |self_, _| async move {
  606. self_.invalidate_draw();
  607. self_.redraw.trigger();
  608. });
  609. on_modify.when_change_external(self.sep_size.prop(), |self_, _| async move {
  610. self_.invalidate_draw();
  611. self_.redraw.trigger();
  612. });
  613. on_modify.when_change_external(self.sep_color.prop(), |self_, _| async move {
  614. self_.invalidate_draw();
  615. self_.redraw.trigger();
  616. });
  617. on_modify.when_change_external(self.fade_zone.prop(), |self_, _| async move {
  618. self_.invalidate_draw();
  619. self_.redraw.trigger();
  620. });
  621. on_modify.when_change_external(self.role1_color.prop(), |self_, _| async move {
  622. self_.invalidate_draw();
  623. self_.redraw.trigger();
  624. });
  625. on_modify.when_change_external(self.role1_group.clone(), |self_, _| async move {
  626. self_.invalidate_draw();
  627. self_.redraw.trigger();
  628. });
  629. on_modify.when_change_external(self.role2_color.prop(), |self_, _| async move {
  630. self_.invalidate_draw();
  631. self_.redraw.trigger();
  632. });
  633. on_modify.when_change_external(self.role2_group.clone(), |self_, _| async move {
  634. self_.invalidate_draw();
  635. self_.redraw.trigger();
  636. });
  637. on_modify.when_change_external(self.window_scale.prop(), |self_, _| async move {
  638. self_.invalidate_draw();
  639. self_.redraw.trigger();
  640. });
  641. let mut tasks = vec![motion_task, cancel_task, done_task];
  642. tasks.append(&mut on_modify.tasks);
  643. *self.tasks.lock() = tasks;
  644. }
  645. fn stop(&self) {
  646. *self.tasks.lock() = vec![];
  647. self.invalidate_draw();
  648. }
  649. async fn draw(
  650. &self,
  651. parent_rect: Rectangle,
  652. atom: &mut PropertyAtomicGuard,
  653. ) -> Option<DrawUpdate> {
  654. *self.parent_rect.lock() = Some(parent_rect);
  655. // Rect property is its own memo: compare before/after eval.
  656. let prev_rect = self.rect.get();
  657. self.rect.eval(atom, &parent_rect).ok()?;
  658. let rect = self.rect.get();
  659. let rect_changed = rect != prev_rect;
  660. // Draw-path re-evaluation of the expr-bound styling props:
  661. // populates the expression caches before the first read (they are
  662. // empty at startup, before any handler has fired) and self-heals
  663. // stale caches from edge cases such as dependency rewiring. Not
  664. // needed for switch atomicity — the batch guard owns that. Cache
  665. // writes are `Role::Internal` echoes, which the
  666. // when_change_external handlers skip.
  667. self.text_color.eval(atom).expect("text_color");
  668. self.bg_color.eval(atom).expect("bg_color");
  669. self.sep_color.eval(atom).expect("sep_color");
  670. self.role1_color.eval(atom).expect("role1_color");
  671. self.role2_color.eval(atom).expect("role2_color");
  672. self.font_size.eval(atom).expect("font_size");
  673. self.sep_size.eval(atom).expect("sep_size");
  674. // The root shell is cheap: re-emit it every pass with the
  675. // current scroll so scroll pokes don't invalidate content.
  676. let scroll = self.scroll.load(Ordering::Relaxed);
  677. // Background meshes are viewport-anchored, so they only depend
  678. // on the rect and content height, never on scroll.
  679. let mut bg = self.bg_meshes.lock();
  680. if bg.is_none() || rect_changed {
  681. *bg = Some(self.make_bg_meshes(&rect));
  682. }
  683. let bg_meshes = bg.clone();
  684. drop(bg);
  685. // Content is reassembled when stale or when the scroll moved:
  686. // only fade-zone items and the separator mesh are rebuilt, the
  687. // rest reuse cached instructions.
  688. let mut cache = self.content_cache.lock();
  689. let stale = match cache.as_ref() {
  690. Some((cached_scroll, _)) => *cached_scroll != scroll,
  691. None => true,
  692. };
  693. if stale || rect_changed {
  694. *cache = Some((scroll, self.assemble_content(&rect, scroll)));
  695. }
  696. let instrs = cache.as_ref().unwrap().1.clone();
  697. drop(cache);
  698. let mut root_dcs = vec![];
  699. let mut draw_calls = vec![(
  700. self.root_dc_key,
  701. DrawCall {
  702. instrs: vec![
  703. DrawInstruction::ApplyView(rect),
  704. DrawInstruction::Move(Point::new(0., -scroll)),
  705. ],
  706. dcs: vec![],
  707. z_index: self.z_index.get(),
  708. debug_str: "menu_root",
  709. },
  710. )];
  711. if let Some((main_mesh, fade_mesh)) = bg_meshes {
  712. root_dcs.push(self.bg_dc_key);
  713. let mut bg_instrs = vec![
  714. DrawInstruction::Move(Point::new(0., scroll)),
  715. DrawInstruction::Draw(main_mesh),
  716. ];
  717. if let Some(fade_mesh) = fade_mesh {
  718. bg_instrs.push(DrawInstruction::Draw(fade_mesh));
  719. }
  720. draw_calls.push((
  721. self.bg_dc_key,
  722. DrawCall { instrs: bg_instrs, dcs: vec![], z_index: 0, debug_str: "menu_bg" },
  723. ));
  724. }
  725. root_dcs.push(self.content_dc_key);
  726. draw_calls[0].1.dcs = root_dcs;
  727. draw_calls.push((
  728. self.content_dc_key,
  729. DrawCall { instrs, dcs: vec![], z_index: 1, debug_str: "menu_content" },
  730. ));
  731. Some(DrawUpdate { key: self.root_dc_key, draw_calls })
  732. }
  733. async fn handle_mouse_btn_down(&self, btn: MouseButton, mouse_pos: Point) -> bool {
  734. if btn != MouseButton::Left {
  735. return false
  736. }
  737. let rect = self.rect.get();
  738. if !rect.contains(mouse_pos) {
  739. return false
  740. }
  741. let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
  742. if is_edit_mode {
  743. let font_size = self.font_size.get();
  744. let hammy_half_size = font_size * 1.4;
  745. let hammy_center = rect.w - MENU_ICON_OFFSET - font_size * 0.56;
  746. let hammy_min = hammy_center - hammy_half_size;
  747. let hammy_max = hammy_center + hammy_half_size;
  748. if mouse_pos.x >= hammy_min && mouse_pos.x <= hammy_max {
  749. if let Some(item_idx) = self.get_selected_item_index(mouse_pos.y) {
  750. *self.drag_info.lock() = Some(DragInfo { item_idx, insert_idx: item_idx });
  751. info!(target: "app::menu", "Dragging item: {}", item_idx);
  752. }
  753. }
  754. }
  755. *self.mouse_click_info.lock() =
  756. Some(MouseClickInfo { start_pos: mouse_pos, start_instant: std::time::Instant::now() });
  757. // Spawn a task to detect long press
  758. let me = self.me.clone();
  759. let start_pos = mouse_pos;
  760. let ex = self.ex.clone();
  761. let long_press_task = ex.spawn(async move {
  762. darkfi::system::msleep(long_press_timeout() as u64).await;
  763. let Some(arc_self) = me.upgrade() else { return };
  764. let current_mouse_pos = arc_self.mouse_pos.lock().clone();
  765. let click_info = arc_self.mouse_click_info.lock().clone();
  766. // Check if button is still held and movement is within threshold
  767. if let Some(info) = click_info {
  768. let movement_dist = ((current_mouse_pos.x - start_pos.x).powi(2) +
  769. (current_mouse_pos.y - start_pos.y).powi(2))
  770. .sqrt();
  771. if movement_dist < LONG_PRESS_EPSILON {
  772. // Long press detected, trigger edit mode
  773. arc_self.save_items_layout();
  774. arc_self.is_edit_mode.store(true, Ordering::Release);
  775. let node = arc_self.node.upgrade().unwrap();
  776. node.trigger("edit_active", vec![]).await.unwrap();
  777. arc_self.invalidate_draw();
  778. arc_self.redraw.trigger();
  779. }
  780. }
  781. });
  782. *self.long_press_task.lock() = Some(long_press_task);
  783. false
  784. }
  785. async fn handle_mouse_btn_up(&self, btn: MouseButton, mouse_pos: Point) -> bool {
  786. if btn != MouseButton::Left {
  787. return false
  788. }
  789. // Apply drag reorder if we were dragging
  790. let drag = self.drag_info.lock().take();
  791. if let Some(drag_info) = drag {
  792. if drag_info.item_idx != drag_info.insert_idx {
  793. let item = self.items.get_str(drag_info.item_idx).unwrap();
  794. let atom = &mut self.redraw.make_guard(gfxtag!("Menu::reorder_item"));
  795. self.items.remove_str(atom, Role::App, drag_info.item_idx).unwrap();
  796. let insert_idx = drag_info.insert_idx;
  797. self.items.insert_str(atom, Role::App, insert_idx, &item).unwrap();
  798. info!(target: "app::menu", "Reordered item {} to {}", drag_info.item_idx, insert_idx);
  799. }
  800. return true
  801. }
  802. // Cancel the long press detection task
  803. let task = self.long_press_task.lock().take();
  804. if let Some(task) = task {
  805. task.cancel().await;
  806. }
  807. let click_info = self.mouse_click_info.lock().take();
  808. let Some(info) = click_info else { return false };
  809. let is_click = (mouse_pos.y - info.start_pos.y).abs() < BIG_EPSILON;
  810. let movement_dist = ((mouse_pos.x - info.start_pos.x).powi(2) +
  811. (mouse_pos.y - info.start_pos.y).powi(2))
  812. .sqrt();
  813. let is_long_press_tap = movement_dist < LONG_PRESS_EPSILON;
  814. let elapsed = info.start_instant.elapsed().as_millis();
  815. self.handle_interaction(mouse_pos, is_click, is_long_press_tap, elapsed).await;
  816. true
  817. }
  818. async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
  819. let rect = self.rect.get();
  820. let mouse_pos = self.mouse_pos.lock().clone();
  821. if !rect.contains(mouse_pos) {
  822. return false
  823. }
  824. self.start_scroll(-wheel_pos.y);
  825. true
  826. }
  827. async fn handle_mouse_move(&self, mouse_pos: Point) -> bool {
  828. *self.mouse_pos.lock() = mouse_pos;
  829. let mut should_redraw = false;
  830. if self.drag_info.lock().is_some() {
  831. if let Some(insert_idx) = self.get_selected_item_index(mouse_pos.y) {
  832. let mut drag = self.drag_info.lock();
  833. if let Some(d) = drag.as_mut() {
  834. if d.insert_idx != insert_idx {
  835. d.insert_idx = insert_idx;
  836. info!(target: "app::menu", "insert_idx changed to: {}", insert_idx);
  837. should_redraw = true;
  838. }
  839. }
  840. }
  841. }
  842. if should_redraw {
  843. self.invalidate_draw();
  844. self.redraw.trigger();
  845. }
  846. false
  847. }
  848. fn gesture_set(&self) -> GestureSet {
  849. GestureSet::MENU
  850. }
  851. fn gesture_hit_test(&self, pos: Point) -> bool {
  852. self.rect.get().contains(pos)
  853. }
  854. async fn handle_gesture(&self, gesture: GestureAction) -> bool {
  855. match gesture {
  856. GestureAction::Down { pos } => {
  857. // Arm the item-reorder grab: touching a reorder handle
  858. // is a zero-threshold action, not a recognized gesture.
  859. if self.is_edit_mode.load(Ordering::Relaxed) {
  860. let rect = self.rect.get();
  861. let font_size = self.font_size.get();
  862. let hammy_half_size = font_size * 2.0;
  863. let hammy_center = rect.w - MENU_ICON_OFFSET - font_size * 0.56;
  864. let hammy_min = hammy_center - hammy_half_size;
  865. let hammy_max = hammy_center + hammy_half_size;
  866. if pos.x >= hammy_min && pos.x <= hammy_max {
  867. if let Some(item_idx) = self.get_selected_item_index(pos.y) {
  868. *self.drag_info.lock() =
  869. Some(DragInfo { item_idx, insert_idx: item_idx });
  870. info!(target: "app::menu", "Dragging item: {}", item_idx);
  871. }
  872. }
  873. }
  874. true
  875. }
  876. GestureAction::DragStart { start } => {
  877. *self.drag_state.lock() = Some((start.y, self.scroll.load(Ordering::Relaxed)));
  878. true
  879. }
  880. GestureAction::DragMove { curr, .. } => {
  881. // An armed reorder takes precedence over scrolling
  882. if self.drag_info.lock().is_some() {
  883. if let Some(insert_idx) = self.get_selected_item_index(curr.y) {
  884. let should_redraw = {
  885. let mut drag = self.drag_info.lock();
  886. match drag.as_mut() {
  887. Some(d) if d.insert_idx != insert_idx => {
  888. d.insert_idx = insert_idx;
  889. info!(target: "app::menu", "insert_idx changed to: {}", insert_idx);
  890. true
  891. }
  892. _ => false,
  893. }
  894. };
  895. if should_redraw {
  896. self.invalidate_draw();
  897. self.redraw.trigger();
  898. }
  899. }
  900. return true
  901. }
  902. let scroll = {
  903. let drag_state = self.drag_state.lock();
  904. let Some((start_y, start_scroll)) = *drag_state else { return false };
  905. start_scroll + start_y - curr.y
  906. };
  907. self.scrollview(scroll);
  908. self.redraw.trigger();
  909. true
  910. }
  911. GestureAction::DragEnd { vel, .. } => {
  912. *self.drag_state.lock() = None;
  913. // Flick inertia from the release velocity, reproducing
  914. // the old dist-over-sample-window formula (px/ms).
  915. let accel = self.scroll_start_accel.get() * -vel.y / 1000.;
  916. self.speed.store(accel, Ordering::Relaxed);
  917. self.motion_cv.notify();
  918. true
  919. }
  920. GestureAction::Up { .. } => {
  921. // Commit an armed reorder at touch end
  922. let drag = self.drag_info.lock().take();
  923. if let Some(drag_info) = drag {
  924. if drag_info.item_idx != drag_info.insert_idx {
  925. let item = self.items.get_str(drag_info.item_idx).unwrap();
  926. let atom = &mut self.redraw.make_guard(gfxtag!("Menu::reorder_item"));
  927. self.items.remove_str(atom, Role::App, drag_info.item_idx).unwrap();
  928. let insert_idx = drag_info.insert_idx;
  929. self.items.insert_str(atom, Role::App, insert_idx, &item).unwrap();
  930. info!(target: "app::menu", "Reordered item {} to {}", drag_info.item_idx, insert_idx);
  931. }
  932. return true
  933. }
  934. false
  935. }
  936. GestureAction::LongPress { .. } => {
  937. // Enter edit mode while the finger is still down; the
  938. // recognizer fires once per touch by construction.
  939. if !self.is_edit_mode.load(Ordering::Relaxed) {
  940. self.save_items_layout();
  941. self.is_edit_mode.store(true, Ordering::Release);
  942. let node = self.node.upgrade().unwrap();
  943. node.trigger("edit_active", vec![]).await.unwrap();
  944. self.invalidate_draw();
  945. self.redraw.trigger();
  946. }
  947. true
  948. }
  949. GestureAction::Tap { pos } => {
  950. // A stationary grab on the reorder handle is a no-op,
  951. // not a selection (the old armed path suppressed it)
  952. if self.drag_info.lock().is_some() {
  953. return true
  954. }
  955. self.handle_interaction(pos, true, false, 0).await;
  956. true
  957. }
  958. }
  959. }
  960. }