mod.rs 43 KB

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