mod.rs 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016
  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, Point, Rectangle, Renderer},
  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. is_visible: PropertyBool,
  110. rect: PropertyRect,
  111. scroll: AtomicF32,
  112. z_index: PropertyUint32,
  113. priority: PropertyUint32,
  114. items: PropertyPtr,
  115. font_size: PropertyFloat32,
  116. padding: PropertyPtr,
  117. handle_padding: PropertyFloat32,
  118. text_color: PropertyColor,
  119. bg_color: PropertyColor,
  120. sep_size: PropertyFloat32,
  121. sep_color: PropertyColor,
  122. role1_color: PropertyColor,
  123. role1_group: PropertyPtr,
  124. role2_color: PropertyColor,
  125. role2_group: PropertyPtr,
  126. fade_zone: PropertyFloat32,
  127. window_scale: PropertyFloat32,
  128. mouse_pos: SyncMutex<Point>,
  129. touch_info: SyncMutex<Option<TouchInfo>>,
  130. mouse_click_info: SyncMutex<Option<MouseClickInfo>>,
  131. drag_info: SyncMutex<Option<DragInfo>>,
  132. long_press_task: SyncMutex<Option<smol::Task<()>>>,
  133. scroll_start_accel: PropertyFloat32,
  134. scroll_resist: PropertyFloat32,
  135. motion_cv: Arc<CondVar>,
  136. speed: AtomicF32,
  137. is_edit_mode: AtomicBool,
  138. parent_rect: SyncMutex<Option<Rectangle>>,
  139. saved_items: SyncMutex<Option<Vec<String>>>,
  140. /// Cached content draw instructions. `None` means stale.
  141. draw_cache: SyncMutex<Option<Vec<DrawInstruction>>>,
  142. }
  143. impl Menu {
  144. pub async fn new(
  145. node: SceneNodeWeak,
  146. window_scale: PropertyFloat32,
  147. renderer: Renderer,
  148. redraw: RedrawTrigger,
  149. ex: ExecutorPtr,
  150. ) -> Pimpl {
  151. let node_ref = &node.upgrade().unwrap();
  152. let is_visible = PropertyBool::wrap(node_ref, Role::Internal, "is_visible", 0).unwrap();
  153. let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
  154. let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
  155. let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
  156. let items = node_ref.get_property("items").expect("Menu::items");
  157. let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
  158. let padding = node_ref.get_property("padding").expect("Menu::padding");
  159. let handle_padding =
  160. PropertyFloat32::wrap(node_ref, Role::Internal, "handle_padding", 0).unwrap();
  161. let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
  162. let bg_color = PropertyColor::wrap(node_ref, Role::Internal, "bg_color").unwrap();
  163. let sep_size = PropertyFloat32::wrap(node_ref, Role::Internal, "sep_size", 0).unwrap();
  164. let sep_color = PropertyColor::wrap(node_ref, Role::Internal, "sep_color").unwrap();
  165. let role1_color = PropertyColor::wrap(node_ref, Role::Internal, "role1_color").unwrap();
  166. let role1_group = node_ref.get_property("role1_group").unwrap();
  167. let role2_color = PropertyColor::wrap(node_ref, Role::Internal, "role2_color").unwrap();
  168. let role2_group = node_ref.get_property("role2_group").unwrap();
  169. let fade_zone = PropertyFloat32::wrap(node_ref, Role::Internal, "fade_zone", 0).unwrap();
  170. let scroll_start_accel =
  171. PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_start_accel", 0).unwrap();
  172. let scroll_resist =
  173. PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_resist", 0).unwrap();
  174. let motion_cv = Arc::new(CondVar::new());
  175. let self_ = Arc::new_cyclic(|me| Self {
  176. node: node.clone(),
  177. me: me.clone(),
  178. ex,
  179. renderer: renderer.clone(),
  180. redraw,
  181. tasks: SyncMutex::new(vec![]),
  182. root_dc_key: OsRng.gen(),
  183. content_dc_key: OsRng.gen(),
  184. is_visible,
  185. rect,
  186. scroll: AtomicF32::new(0.),
  187. z_index,
  188. priority,
  189. items,
  190. font_size,
  191. padding,
  192. handle_padding,
  193. text_color,
  194. bg_color,
  195. sep_size,
  196. sep_color,
  197. role1_color,
  198. role1_group,
  199. role2_color,
  200. role2_group,
  201. fade_zone,
  202. window_scale,
  203. mouse_pos: SyncMutex::new(Point::new(0., 0.)),
  204. touch_info: SyncMutex::new(None),
  205. mouse_click_info: SyncMutex::new(None),
  206. drag_info: SyncMutex::new(None),
  207. long_press_task: SyncMutex::new(None),
  208. scroll_start_accel,
  209. scroll_resist,
  210. motion_cv,
  211. speed: AtomicF32::new(0.),
  212. is_edit_mode: AtomicBool::new(false),
  213. parent_rect: SyncMutex::new(None),
  214. saved_items: SyncMutex::new(None),
  215. draw_cache: SyncMutex::new(None),
  216. });
  217. Pimpl::Menu(self_)
  218. }
  219. /// Height of a single item
  220. fn get_item_height(&self) -> f32 {
  221. self.font_size.get() + self.padding.get_f32(1).unwrap() * 2.0
  222. }
  223. /// Save the current menu items layout
  224. fn save_items_layout(&self) {
  225. let items = self.items.get_str_vec().unwrap();
  226. *self.saved_items.lock() = Some(items);
  227. }
  228. /// Height of the content without the overscroll
  229. fn content_height(&self) -> f32 {
  230. self.items.get_len() as f32 * self.get_item_height()
  231. }
  232. fn get_selected_item_index(&self, click_y: f32) -> Option<usize> {
  233. let rect = self.rect.get();
  234. let scroll = self.scroll.load(Ordering::Relaxed);
  235. // Scroll is positive value so to translate click into content, we must add the scroll.
  236. let content_y = click_y + scroll - rect.y;
  237. if content_y < 0. || content_y > self.content_height() {
  238. return None
  239. }
  240. let item_height = self.get_item_height();
  241. Some((content_y / item_height) as usize)
  242. }
  243. async fn handle_selection(&self, item_idx: usize) {
  244. if item_idx < self.items.get_len() {
  245. let node = self.node.upgrade().unwrap();
  246. let item_name = self.items.get_str(item_idx).unwrap();
  247. let data = serialize(&item_name);
  248. node.trigger("select", data).await.unwrap();
  249. }
  250. }
  251. async fn handle_interaction(
  252. &self,
  253. pos: Point,
  254. is_tap: bool,
  255. is_long_press_tap: bool,
  256. elapsed_ms: u128,
  257. ) {
  258. let is_long_press = is_long_press_tap && elapsed_ms >= long_press_timeout() as u128;
  259. if is_long_press {
  260. if !self.is_edit_mode.load(Ordering::Relaxed) {
  261. self.save_items_layout();
  262. self.is_edit_mode.store(true, Ordering::Release);
  263. let node = self.node.upgrade().unwrap();
  264. node.trigger("edit_active", vec![]).await.unwrap();
  265. *self.draw_cache.lock() = None;
  266. self.redraw.trigger();
  267. }
  268. } else if is_tap {
  269. let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
  270. if is_edit_mode {
  271. let font_size = self.font_size.get();
  272. let x_half_size = font_size * 0.85;
  273. let x_center = MENU_ICON_OFFSET + font_size * 0.4;
  274. if let Some(item_idx) = self.get_selected_item_index(pos.y) {
  275. let item_name = self.items.get_str(item_idx).unwrap();
  276. let x_min = x_center - x_half_size;
  277. let x_max = x_center + x_half_size;
  278. if pos.x >= x_min && pos.x <= x_max {
  279. info!(target: "app::menu", "X clicked for item: {item_name}");
  280. let atom = &mut self.renderer.make_guard(gfxtag!("Menu::delete_item"));
  281. self.items.remove_str(atom, Role::App, item_idx).unwrap();
  282. } else {
  283. self.handle_selection(item_idx).await;
  284. }
  285. }
  286. } else if let Some(item_idx) = self.get_selected_item_index(pos.y) {
  287. self.handle_selection(item_idx).await;
  288. }
  289. }
  290. }
  291. /// Build the content draw instructions. Called only when the draw
  292. /// cache is stale or the rect changed; the root shell is re-emitted
  293. /// cheaply by the draw pass on every pass.
  294. fn make_content_instrs(&self, rect: &Rectangle) -> Vec<DrawInstruction> {
  295. let mut instrs = vec![];
  296. let scroll = self.scroll.load(Ordering::Relaxed);
  297. let item_height = self.get_item_height();
  298. let font_size = self.font_size.get();
  299. let padding_x = self.padding.get_f32(0).unwrap();
  300. let padding_y = self.padding.get_f32(1).unwrap();
  301. let handle_padding = self.handle_padding.get();
  302. let text_color = self.text_color.get();
  303. let role1_color = self.role1_color.get();
  304. let role2_color = self.role2_color.get();
  305. let bg_color = self.bg_color.get();
  306. let sep_size = self.sep_size.get();
  307. let sep_color = self.sep_color.get();
  308. let fade_distance = self.fade_zone.get();
  309. let window_scale = self.window_scale.get();
  310. let role1_set: HashSet<String> =
  311. self.role1_group.get_str_vec().unwrap_or_default().into_iter().collect();
  312. let role2_set: HashSet<String> =
  313. self.role2_group.get_str_vec().unwrap_or_default().into_iter().collect();
  314. let num_items = self.items.get_len();
  315. // Get items and reorder if dragging
  316. let mut items_list = {
  317. let mut items = vec![];
  318. for idx in 0..num_items {
  319. items.push(self.items.get_str(idx).unwrap());
  320. }
  321. items
  322. };
  323. if let Some(ref drag_info) = self.drag_info.lock().as_ref() {
  324. if drag_info.item_idx != drag_info.insert_idx {
  325. let item = items_list.remove(drag_info.item_idx);
  326. items_list.insert(drag_info.insert_idx, item);
  327. }
  328. }
  329. // Draw single background mesh for the entire menu
  330. let content_height = num_items as f32 * item_height;
  331. let mut bg_mesh = MeshBuilder::new(gfxtag!("menu_bg"));
  332. bg_mesh.draw_filled_box(&Rectangle::new(0., 0., rect.w, content_height), bg_color);
  333. let bg_mesh = bg_mesh.alloc(&self.renderer).draw_untextured();
  334. instrs.push(DrawInstruction::Draw(bg_mesh));
  335. // Separator line mesh
  336. let mut sep_mesh = MeshBuilder::new(gfxtag!("menu_sep"));
  337. sep_mesh.draw_filled_box(&Rectangle::new(0., 0., rect.w, sep_size), sep_color);
  338. let sep_mesh = sep_mesh.alloc(&self.renderer).draw_untextured();
  339. let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
  340. let edit_offset = if is_edit_mode { handle_padding } else { 0.0 };
  341. // Create X mesh for edit mode
  342. let x_mesh =
  343. if is_edit_mode { Some(shape::make_x(&self.renderer, font_size)) } else { None };
  344. let mut edit_instrs = vec![];
  345. if is_edit_mode {
  346. let item_center_y = item_height / 2.0;
  347. let x_center = MENU_ICON_OFFSET + font_size * 0.4;
  348. let rhs = rect.w - MENU_ICON_OFFSET - font_size * 0.56;
  349. edit_instrs.push(DrawInstruction::Move(Point::new(x_center, item_center_y)));
  350. edit_instrs.push(DrawInstruction::Draw(shape::make_x(&self.renderer, font_size)));
  351. edit_instrs.push(DrawInstruction::Move(Point::new(-x_center, -item_center_y)));
  352. edit_instrs.push(DrawInstruction::Move(Point::new(rhs, item_center_y)));
  353. edit_instrs.push(DrawInstruction::Draw(shape::make_hammy(&self.renderer, font_size)));
  354. edit_instrs.push(DrawInstruction::Move(Point::new(-rhs, -item_center_y)));
  355. }
  356. for idx in 0..num_items {
  357. let item_text = items_list[idx].clone();
  358. let base_color = if role2_set.contains(&item_text) {
  359. role2_color
  360. } else if role1_set.contains(&item_text) {
  361. role1_color
  362. } else {
  363. text_color
  364. };
  365. // Apply fade effect in the configured fade zone
  366. let item_y = idx as f32 * item_height - scroll;
  367. let fade_zone_start = rect.h - fade_distance;
  368. let color = if item_y >= fade_zone_start {
  369. let fade_factor =
  370. 1.0 - ((item_y - fade_zone_start) / fade_distance).clamp(0.0, 1.0);
  371. let mut faded = base_color;
  372. faded[3] *= fade_factor;
  373. faded
  374. } else {
  375. base_color
  376. };
  377. instrs.append(&mut edit_instrs.clone());
  378. // Draw text
  379. let layout = text::make_layout(
  380. &item_text,
  381. color,
  382. font_size,
  383. 1.0,
  384. window_scale,
  385. Some(rect.w - padding_x * 2.),
  386. &[],
  387. );
  388. let text_instr = text::render_layout(&layout, &self.renderer, gfxtag!("menu_text"));
  389. // Use a fraction of edit_offset for the label position to reduce gap from X icon
  390. let label_edit_offset = edit_offset * 0.62;
  391. instrs
  392. .push(DrawInstruction::Move(Point::new(padding_x + label_edit_offset, padding_y)));
  393. instrs.extend(text_instr);
  394. instrs.push(DrawInstruction::Move(Point::new(
  395. -padding_x - label_edit_offset,
  396. font_size + padding_y,
  397. )));
  398. // Draw separator (except for last item)
  399. if idx < num_items - 1 {
  400. instrs.push(DrawInstruction::Draw(sep_mesh.clone()));
  401. }
  402. }
  403. instrs
  404. }
  405. fn scrollview(&self, scroll: f32) {
  406. let item_height = self.get_item_height();
  407. let num_items = self.items.get_len() as f32;
  408. let content_height = num_items * item_height;
  409. let rect = self.rect.get();
  410. let max_scroll = (content_height - rect.h).max(0.);
  411. let scroll = scroll.clamp(0., max_scroll);
  412. self.scroll.store(scroll, Ordering::Relaxed);
  413. }
  414. fn start_scroll(&self, delta: f32) {
  415. let accel = self.scroll_start_accel.get();
  416. self.speed.store(delta * accel, Ordering::Relaxed);
  417. self.motion_cv.notify();
  418. }
  419. async fn handle_movement(&self) {
  420. let resist = self.scroll_resist.get();
  421. loop {
  422. let mut speed = self.speed.load(Ordering::Relaxed);
  423. if speed.abs() < EPSILON {
  424. break
  425. }
  426. while speed.abs() >= EPSILON {
  427. speed = self.speed.load(Ordering::Relaxed);
  428. let scroll = self.scroll.load(Ordering::Relaxed);
  429. self.scrollview(scroll + speed);
  430. self.redraw.trigger();
  431. speed *= resist;
  432. self.speed.store(speed, Ordering::Relaxed);
  433. darkfi::system::msleep(16).await;
  434. }
  435. self.speed.store(0., Ordering::Relaxed);
  436. break
  437. }
  438. }
  439. fn end_touch_phase(&self, touch_y: f32) {
  440. let touch_info = std::mem::take(&mut *self.touch_info.lock());
  441. let info = touch_info.unwrap();
  442. if let Some((dt, _)) = info.first_sample() {
  443. if dt > EPSILON {
  444. let velocity = (touch_y - info.start_pos.y) / dt;
  445. self.start_scroll(-velocity);
  446. }
  447. }
  448. }
  449. /// Cancels edit mode changes, reverting any modifications made during edit mode
  450. async fn process_cancel_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
  451. let Ok(method_call) = sub.receive().await else {
  452. d!("Event relayer closed");
  453. return false
  454. };
  455. d!("method called: cancel({method_call:?})");
  456. assert!(method_call.send_res.is_none());
  457. let Some(self_) = me.upgrade() else {
  458. d!("Self destroyed");
  459. return true
  460. };
  461. let atom = &mut self_.renderer.make_guard(gfxtag!("Menu::cancel_edit"));
  462. // Restore the saved items
  463. // It must exist otherwise theres a logic err
  464. let saved_items = self_.saved_items.lock().take().unwrap();
  465. self_.items.set_str_vec(atom, Role::App, saved_items).unwrap();
  466. // Exit edit mode
  467. self_.is_edit_mode.store(false, Ordering::Release);
  468. *self_.draw_cache.lock() = None;
  469. self_.redraw.trigger();
  470. true
  471. }
  472. /// Accepts edit mode changes, finalizing any modifications made during edit mode
  473. async fn process_done_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
  474. let Ok(method_call) = sub.receive().await else {
  475. d!("Event relayer closed");
  476. return false
  477. };
  478. d!("method called: done({method_call:?})");
  479. assert!(method_call.send_res.is_none());
  480. let Some(self_) = me.upgrade() else {
  481. d!("Self destroyed");
  482. return true
  483. };
  484. // Calculate deleted items by diffing saved and current items
  485. let saved_items = self_.saved_items.lock().take().unwrap();
  486. let current_items = self_.items.get_str_vec().unwrap();
  487. let deleted_items: Vec<String> =
  488. saved_items.into_iter().filter(|item| !current_items.contains(item)).collect();
  489. // Send the edit_done signal with deleted items
  490. let node = self_.node.upgrade().unwrap();
  491. let data = serialize(&deleted_items);
  492. node.trigger("edit_done", data).await.unwrap();
  493. self_.is_edit_mode.store(false, Ordering::Release);
  494. *self_.draw_cache.lock() = None;
  495. self_.redraw.trigger();
  496. true
  497. }
  498. }
  499. #[async_trait]
  500. impl UIObject for Menu {
  501. fn priority(&self) -> u32 {
  502. self.priority.get()
  503. }
  504. async fn start(self: Arc<Self>, ex: ExecutorPtr) {
  505. let me = self.me.clone();
  506. let node_ref = &self.node.upgrade().unwrap();
  507. let me2 = me.clone();
  508. let cv = self.motion_cv.clone();
  509. let motion_task = ex.spawn(async move {
  510. loop {
  511. cv.wait().await;
  512. let Some(self_) = me2.upgrade() else {
  513. t!("Menu destroyed before motion_task stopped");
  514. break;
  515. };
  516. self_.handle_movement().await;
  517. cv.reset();
  518. }
  519. });
  520. let method_sub = node_ref.subscribe_method_call("cancel_edit").unwrap();
  521. let me2 = me.clone();
  522. let cancel_task =
  523. ex.spawn(async move { while Self::process_cancel_method(&me2, &method_sub).await {} });
  524. let method_sub = node_ref.subscribe_method_call("done_edit").unwrap();
  525. let me2 = me.clone();
  526. let done_task =
  527. ex.spawn(async move { while Self::process_done_method(&me2, &method_sub).await {} });
  528. let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
  529. on_modify.when_change_external(self.items.clone(), |self_, _| async move {
  530. *self_.draw_cache.lock() = None;
  531. self_.redraw.trigger();
  532. });
  533. on_modify.when_change_external(self.rect.prop(), |self_, _| async move {
  534. *self_.draw_cache.lock() = None;
  535. self_.redraw.trigger();
  536. });
  537. on_modify.when_change_external(self.font_size.prop(), |self_, _| async move {
  538. *self_.draw_cache.lock() = None;
  539. self_.redraw.trigger();
  540. });
  541. on_modify.when_change_external(self.padding.clone(), |self_, _| async move {
  542. *self_.draw_cache.lock() = None;
  543. self_.redraw.trigger();
  544. });
  545. on_modify.when_change_external(self.text_color.prop(), |self_, _| async move {
  546. *self_.draw_cache.lock() = None;
  547. self_.redraw.trigger();
  548. });
  549. on_modify.when_change_external(self.bg_color.prop(), |self_, _| async move {
  550. *self_.draw_cache.lock() = None;
  551. self_.redraw.trigger();
  552. });
  553. on_modify.when_change_external(self.sep_size.prop(), |self_, _| async move {
  554. *self_.draw_cache.lock() = None;
  555. self_.redraw.trigger();
  556. });
  557. on_modify.when_change_external(self.sep_color.prop(), |self_, _| async move {
  558. *self_.draw_cache.lock() = None;
  559. self_.redraw.trigger();
  560. });
  561. on_modify.when_change_external(self.role1_color.prop(), |self_, _| async move {
  562. *self_.draw_cache.lock() = None;
  563. self_.redraw.trigger();
  564. });
  565. on_modify.when_change_external(self.role1_group.clone(), |self_, _| async move {
  566. *self_.draw_cache.lock() = None;
  567. self_.redraw.trigger();
  568. });
  569. on_modify.when_change_external(self.role2_color.prop(), |self_, _| async move {
  570. *self_.draw_cache.lock() = None;
  571. self_.redraw.trigger();
  572. });
  573. on_modify.when_change_external(self.role2_group.clone(), |self_, _| async move {
  574. *self_.draw_cache.lock() = None;
  575. self_.redraw.trigger();
  576. });
  577. on_modify.when_change_external(self.window_scale.prop(), |self_, _| async move {
  578. *self_.draw_cache.lock() = None;
  579. self_.redraw.trigger();
  580. });
  581. let mut tasks = vec![motion_task, cancel_task, done_task];
  582. tasks.append(&mut on_modify.tasks);
  583. *self.tasks.lock() = tasks;
  584. }
  585. fn stop(&self) {
  586. *self.tasks.lock() = vec![];
  587. *self.draw_cache.lock() = None;
  588. }
  589. async fn draw(
  590. &self,
  591. parent_rect: Rectangle,
  592. atom: &mut PropertyAtomicGuard,
  593. ) -> Option<DrawUpdate> {
  594. *self.parent_rect.lock() = Some(parent_rect);
  595. // Rect property is its own memo: compare before/after eval.
  596. let prev_rect = self.rect.get();
  597. self.rect.eval(atom, &parent_rect).ok()?;
  598. let rect = self.rect.get();
  599. let rect_changed = rect != prev_rect;
  600. // Compute under the lock so a concurrent invalidation lands
  601. // before or after, never between.
  602. let mut cache = self.draw_cache.lock();
  603. if cache.is_none() || rect_changed {
  604. *cache = Some(self.make_content_instrs(&rect));
  605. }
  606. let instrs = cache.clone().unwrap();
  607. drop(cache);
  608. // The root shell is cheap: re-emit it every pass with the
  609. // current scroll so scroll pokes don't invalidate content.
  610. let scroll = self.scroll.load(Ordering::Relaxed);
  611. Some(DrawUpdate {
  612. key: self.root_dc_key,
  613. draw_calls: vec![
  614. (
  615. self.root_dc_key,
  616. DrawCall {
  617. instrs: vec![
  618. DrawInstruction::ApplyView(rect),
  619. DrawInstruction::Move(Point::new(0., -scroll)),
  620. ],
  621. dcs: vec![self.content_dc_key],
  622. z_index: self.z_index.get(),
  623. debug_str: "menu_root",
  624. },
  625. ),
  626. (
  627. self.content_dc_key,
  628. DrawCall {
  629. instrs,
  630. dcs: vec![],
  631. z_index: self.z_index.get(),
  632. debug_str: "menu_content",
  633. },
  634. ),
  635. ],
  636. })
  637. }
  638. async fn handle_mouse_btn_down(&self, btn: MouseButton, mouse_pos: Point) -> bool {
  639. if btn != MouseButton::Left {
  640. return false
  641. }
  642. let rect = self.rect.get();
  643. if !rect.contains(mouse_pos) {
  644. return false
  645. }
  646. let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
  647. if is_edit_mode {
  648. let font_size = self.font_size.get();
  649. let hammy_half_size = font_size * 1.4;
  650. let hammy_center = rect.w - MENU_ICON_OFFSET - font_size * 0.56;
  651. let hammy_min = hammy_center - hammy_half_size;
  652. let hammy_max = hammy_center + hammy_half_size;
  653. if mouse_pos.x >= hammy_min && mouse_pos.x <= hammy_max {
  654. if let Some(item_idx) = self.get_selected_item_index(mouse_pos.y) {
  655. *self.drag_info.lock() = Some(DragInfo { item_idx, insert_idx: item_idx });
  656. info!(target: "app::menu", "Dragging item: {}", item_idx);
  657. }
  658. }
  659. }
  660. *self.mouse_click_info.lock() =
  661. Some(MouseClickInfo { start_pos: mouse_pos, start_instant: std::time::Instant::now() });
  662. // Spawn a task to detect long press
  663. let me = self.me.clone();
  664. let start_pos = mouse_pos;
  665. let ex = self.ex.clone();
  666. let long_press_task = ex.spawn(async move {
  667. darkfi::system::msleep(long_press_timeout() as u64).await;
  668. let Some(arc_self) = me.upgrade() else { return };
  669. let current_mouse_pos = arc_self.mouse_pos.lock().clone();
  670. let click_info = arc_self.mouse_click_info.lock().clone();
  671. // Check if button is still held and movement is within threshold
  672. if let Some(info) = click_info {
  673. let movement_dist = ((current_mouse_pos.x - start_pos.x).powi(2) +
  674. (current_mouse_pos.y - start_pos.y).powi(2))
  675. .sqrt();
  676. if movement_dist < LONG_PRESS_EPSILON {
  677. // Long press detected, trigger edit mode
  678. arc_self.save_items_layout();
  679. arc_self.is_edit_mode.store(true, Ordering::Release);
  680. let node = arc_self.node.upgrade().unwrap();
  681. node.trigger("edit_active", vec![]).await.unwrap();
  682. *arc_self.draw_cache.lock() = None;
  683. arc_self.redraw.trigger();
  684. }
  685. }
  686. });
  687. *self.long_press_task.lock() = Some(long_press_task);
  688. false
  689. }
  690. async fn handle_mouse_btn_up(&self, btn: MouseButton, mouse_pos: Point) -> bool {
  691. if btn != MouseButton::Left {
  692. return false
  693. }
  694. // Apply drag reorder if we were dragging
  695. let drag = self.drag_info.lock().take();
  696. if let Some(drag_info) = drag {
  697. if drag_info.item_idx != drag_info.insert_idx {
  698. let item = self.items.get_str(drag_info.item_idx).unwrap();
  699. let atom = &mut self.renderer.make_guard(gfxtag!("Menu::reorder_item"));
  700. self.items.remove_str(atom, Role::App, drag_info.item_idx).unwrap();
  701. let insert_idx = drag_info.insert_idx;
  702. self.items.insert_str(atom, Role::App, insert_idx, &item).unwrap();
  703. info!(target: "app::menu", "Reordered item {} to {}", drag_info.item_idx, insert_idx);
  704. }
  705. return true
  706. }
  707. // Cancel the long press detection task
  708. let task = self.long_press_task.lock().take();
  709. if let Some(task) = task {
  710. task.cancel().await;
  711. }
  712. let click_info = self.mouse_click_info.lock().take();
  713. let Some(info) = click_info else { return false };
  714. let is_click = (mouse_pos.y - info.start_pos.y).abs() < BIG_EPSILON;
  715. let movement_dist = ((mouse_pos.x - info.start_pos.x).powi(2) +
  716. (mouse_pos.y - info.start_pos.y).powi(2))
  717. .sqrt();
  718. let is_long_press_tap = movement_dist < LONG_PRESS_EPSILON;
  719. let elapsed = info.start_instant.elapsed().as_millis();
  720. self.handle_interaction(mouse_pos, is_click, is_long_press_tap, elapsed).await;
  721. true
  722. }
  723. async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
  724. let rect = self.rect.get();
  725. let mouse_pos = self.mouse_pos.lock().clone();
  726. if !rect.contains(mouse_pos) {
  727. return false
  728. }
  729. self.start_scroll(-wheel_pos.y);
  730. true
  731. }
  732. async fn handle_mouse_move(&self, mouse_pos: Point) -> bool {
  733. *self.mouse_pos.lock() = mouse_pos;
  734. let mut should_redraw = false;
  735. if self.drag_info.lock().is_some() {
  736. if let Some(insert_idx) = self.get_selected_item_index(mouse_pos.y) {
  737. let mut drag = self.drag_info.lock();
  738. if let Some(d) = drag.as_mut() {
  739. if d.insert_idx != insert_idx {
  740. d.insert_idx = insert_idx;
  741. info!(target: "app::menu", "insert_idx changed to: {}", insert_idx);
  742. should_redraw = true;
  743. }
  744. }
  745. }
  746. }
  747. if should_redraw {
  748. *self.draw_cache.lock() = None;
  749. self.redraw.trigger();
  750. }
  751. false
  752. }
  753. fn handle_touch_sync(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
  754. if id != 0 {
  755. return false
  756. }
  757. match phase {
  758. TouchPhase::Started => {
  759. let rect = self.rect.get();
  760. if !rect.contains(touch_pos) {
  761. *self.touch_info.lock() = None;
  762. return false
  763. }
  764. let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
  765. if is_edit_mode {
  766. let font_size = self.font_size.get();
  767. let hammy_half_size = font_size * 2.0;
  768. let hammy_center = rect.w - MENU_ICON_OFFSET - font_size * 0.56;
  769. let hammy_min = hammy_center - hammy_half_size;
  770. let hammy_max = hammy_center + hammy_half_size;
  771. if touch_pos.x >= hammy_min && touch_pos.x <= hammy_max {
  772. if let Some(item_idx) = self.get_selected_item_index(touch_pos.y) {
  773. *self.drag_info.lock() =
  774. Some(DragInfo { item_idx, insert_idx: item_idx });
  775. info!(target: "app::menu", "Dragging item: {}", item_idx);
  776. }
  777. }
  778. }
  779. *self.touch_info.lock() =
  780. Some(TouchInfo::new(self.scroll.load(Ordering::Relaxed), touch_pos));
  781. true
  782. }
  783. TouchPhase::Moved => {
  784. let mut should_redraw = false;
  785. if self.drag_info.lock().is_some() {
  786. if let Some(insert_idx) = self.get_selected_item_index(touch_pos.y) {
  787. let mut drag = self.drag_info.lock();
  788. if let Some(d) = drag.as_mut() {
  789. if d.insert_idx != insert_idx {
  790. d.insert_idx = insert_idx;
  791. info!(target: "app::menu", "insert_idx changed to: {}", insert_idx);
  792. should_redraw = true;
  793. }
  794. }
  795. }
  796. }
  797. if should_redraw {
  798. *self.draw_cache.lock() = None;
  799. self.redraw.trigger();
  800. }
  801. let scroll = {
  802. let mut touch_info = self.touch_info.lock();
  803. let Some(info) = &mut *touch_info else { return false };
  804. info.last_pos = touch_pos;
  805. info.push_sample(touch_pos.y);
  806. let last_elapsed = info.last_instant.elapsed().as_millis();
  807. if last_elapsed <= 20 {
  808. return true
  809. }
  810. info.last_instant = std::time::Instant::now();
  811. let dist = touch_pos.y - info.start_pos.y;
  812. if dist.abs() < BIG_EPSILON {
  813. return true
  814. }
  815. info.start_scroll - dist
  816. };
  817. self.scrollview(scroll);
  818. self.redraw.trigger();
  819. true
  820. }
  821. // Use async handler instead
  822. TouchPhase::Ended | TouchPhase::Cancelled => false,
  823. }
  824. }
  825. async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
  826. if id != 0 {
  827. return false
  828. }
  829. match phase {
  830. // Should be handled by handle_touch_sync
  831. TouchPhase::Started | TouchPhase::Moved => false,
  832. TouchPhase::Ended | TouchPhase::Cancelled => {
  833. let drag = self.drag_info.lock().take();
  834. if let Some(drag_info) = drag {
  835. if drag_info.item_idx != drag_info.insert_idx {
  836. let item = self.items.get_str(drag_info.item_idx).unwrap();
  837. let atom = &mut self.renderer.make_guard(gfxtag!("Menu::reorder_item"));
  838. self.items.remove_str(atom, Role::App, drag_info.item_idx).unwrap();
  839. let insert_idx = drag_info.insert_idx;
  840. self.items.insert_str(atom, Role::App, insert_idx, &item).unwrap();
  841. info!(target: "app::menu", "Reordered item {} to {}", drag_info.item_idx, insert_idx);
  842. }
  843. return true
  844. }
  845. let (is_tap, is_long_press_tap, elapsed) = {
  846. let touch_info = self.touch_info.lock();
  847. let Some(info) = &*touch_info else { return true };
  848. let is_tap = (touch_pos.y - info.start_pos.y).abs() < BIG_EPSILON;
  849. let movement_dist = ((touch_pos.x - info.start_pos.x).powi(2) +
  850. (touch_pos.y - info.start_pos.y).powi(2))
  851. .sqrt();
  852. let is_long_press_tap = movement_dist < LONG_PRESS_EPSILON;
  853. let elapsed = info.start_instant.elapsed().as_millis();
  854. (is_tap, is_long_press_tap, elapsed)
  855. };
  856. self.handle_interaction(touch_pos, is_tap, is_long_press_tap, elapsed).await;
  857. self.end_touch_phase(touch_pos.y);
  858. true
  859. }
  860. }
  861. }
  862. }