mod.rs 43 KB

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