Jelajahi Sumber

app/menu: long click hold show buttons to edit entries

jkds 6 bulan lalu
induk
melakukan
843911e94b

+ 6 - 0
bin/app/src/app/node.rs

@@ -627,6 +627,12 @@ pub fn create_menu(name: &str) -> SceneNode {
     prop.set_range_f32(0., f32::MAX);
     node.add_property(prop).unwrap();
 
+    let mut prop = Property::new("handle_padding", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_ui_text("Handle Padding", "X handle padding from left edge");
+    prop.set_defaults_f32(vec![14.0]).unwrap();
+    prop.set_range_f32(0., f32::MAX);
+    node.add_property(prop).unwrap();
+
     let mut prop = Property::new("text_color", PropertyType::Float32, PropertySubType::Color);
     prop.set_ui_text("Text Color", "Text color (RGBA)");
     prop.set_array_len(4);

+ 4 - 0
bin/app/src/app/schema/menu.rs

@@ -43,6 +43,7 @@ mod android_ui_consts {
     pub const CHANNEL_LABEL_LINESPACE: f32 = 140.;
     pub const CHANNEL_LABEL_FONTSIZE: f32 = 44.;
     pub const MENU_SEP_SIZE: f32 = 3.;
+    pub const MENU_HANDLE_PAD: f32 = 100.;
     pub const VERBLOCK_SCALE: f32 = 150.;
     pub const VERBLOCK_X: f32 = 180.;
     pub const VERBLOCK_Y: f32 = 80.;
@@ -68,6 +69,7 @@ mod ui_consts {
     pub const CHANNEL_LABEL_LINESPACE: f32 = 60.;
     pub const CHANNEL_LABEL_FONTSIZE: f32 = 22.;
     pub const MENU_SEP_SIZE: f32 = 1.;
+    pub const MENU_HANDLE_PAD: f32 = 50.;
     pub const VERBLOCK_SCALE: f32 = 80.;
     pub const VERBLOCK_X: f32 = 110.;
     pub const VERBLOCK_Y: f32 = 50.;
@@ -221,6 +223,8 @@ pub async fn make(app: &App, content: SceneNodePtr, i18n_fish: &I18nBabelFish) {
     prop.set_f32(atom, Role::App, 0, CHANNEL_LABEL_X).unwrap();
     prop.set_f32(atom, Role::App, 1, CHANNEL_LABEL_LINESPACE / 2.).unwrap();
 
+    node.set_property_f32(atom, Role::App, "handle_padding", MENU_HANDLE_PAD).unwrap();
+
     let prop = node.get_property("items").unwrap();
     for channel in CHANNELS {
         let label = "#".to_string() + channel;

+ 104 - 22
bin/app/src/ui/menu.rs → bin/app/src/ui/menu/mod.rs

@@ -45,8 +45,11 @@ use crate::{
 
 use super::{DrawUpdate, OnModify, UIObject};
 
+mod shape;
+
 const EPSILON: f32 = 0.001;
 const BIG_EPSILON: f32 = 0.05;
+const LONG_PRESS_EPSILON: f32 = 5.0;
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::menu", $($arg)*); } }
 
@@ -59,22 +62,28 @@ enum ItemStatus {
 #[derive(Clone)]
 struct TouchInfo {
     start_scroll: f32,
-    start_y: f32,
+    start_pos: Point,
     start_instant: std::time::Instant,
     samples: VecDeque<(std::time::Instant, f32)>,
     last_instant: std::time::Instant,
     last_y: f32,
 }
 
+#[derive(Clone)]
+struct MouseClickInfo {
+    start_pos: Point,
+    start_instant: std::time::Instant,
+}
+
 impl TouchInfo {
-    fn new(start_scroll: f32, y: f32) -> Self {
+    fn new(start_scroll: f32, pos: Point) -> Self {
         Self {
             start_scroll,
-            start_y: y,
+            start_pos: pos,
             start_instant: std::time::Instant::now(),
-            samples: VecDeque::from([(std::time::Instant::now(), y)]),
+            samples: VecDeque::from([(std::time::Instant::now(), pos.y)]),
             last_instant: std::time::Instant::now(),
-            last_y: y,
+            last_y: pos.y,
         }
     }
 
@@ -112,6 +121,7 @@ pub struct Menu {
 
     font_size: PropertyFloat32,
     padding: PropertyPtr,
+    handle_padding: PropertyFloat32,
     text_color: PropertyColor,
     bg_color: PropertyColor,
     sep_size: PropertyFloat32,
@@ -122,10 +132,12 @@ pub struct Menu {
 
     mouse_pos: SyncMutex<Point>,
     touch_info: SyncMutex<Option<TouchInfo>>,
+    mouse_click_info: SyncMutex<Option<MouseClickInfo>>,
     scroll_start_accel: PropertyFloat32,
     scroll_resist: PropertyFloat32,
     motion_cv: Arc<CondVar>,
     speed: AtomicF32,
+    is_edit_mode: AtomicBool,
 
     parent_rect: SyncMutex<Option<Rectangle>>,
     item_states: SyncMutex<HashMap<String, ItemStatus>>,
@@ -146,6 +158,8 @@ impl Menu {
 
         let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
         let padding = node_ref.get_property("padding").expect("Menu::padding");
+        let handle_padding =
+            PropertyFloat32::wrap(node_ref, Role::Internal, "handle_padding", 0).unwrap();
         let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
         let bg_color = PropertyColor::wrap(node_ref, Role::Internal, "bg_color").unwrap();
         let sep_size = PropertyFloat32::wrap(node_ref, Role::Internal, "sep_size", 0).unwrap();
@@ -174,6 +188,7 @@ impl Menu {
             items,
             font_size,
             padding,
+            handle_padding,
             text_color,
             bg_color,
             sep_size,
@@ -183,10 +198,12 @@ impl Menu {
             window_scale,
             mouse_pos: SyncMutex::new(Point::new(0., 0.)),
             touch_info: SyncMutex::new(None),
+            mouse_click_info: SyncMutex::new(None),
             scroll_start_accel,
             scroll_resist,
             motion_cv,
             speed: AtomicF32::new(0.),
+            is_edit_mode: AtomicBool::new(false),
             parent_rect: SyncMutex::new(None),
             item_states: SyncMutex::new(HashMap::new()),
         });
@@ -229,6 +246,26 @@ impl Menu {
         }
     }
 
+    async fn handle_interaction(
+        &self,
+        y: f32,
+        is_tap: bool,
+        is_long_press_tap: bool,
+        elapsed_ms: u128,
+    ) {
+        let is_long_press = is_long_press_tap && elapsed_ms >= 500;
+
+        if is_long_press {
+            self.is_edit_mode.store(true, Ordering::Release);
+            let atom = &mut self.renderer.make_guard(gfxtag!("Menu::long_press"));
+            self.redraw(atom);
+        } else if is_tap {
+            if let Some(item_idx) = self.get_selected_item_index(y) {
+                self.handle_selection(item_idx).await;
+            }
+        }
+    }
+
     fn get_draw_calls(
         &self,
         atom: &mut PropertyAtomicGuard,
@@ -244,6 +281,7 @@ impl Menu {
         let font_size = self.font_size.get();
         let padding_x = self.padding.get_f32(0).unwrap();
         let padding_y = self.padding.get_f32(1).unwrap();
+        let handle_padding = self.handle_padding.get();
         let text_color = self.text_color.get();
         let active_color = self.active_color.get();
         let alert_color = self.alert_color.get();
@@ -269,6 +307,25 @@ impl Menu {
         let sep_mesh = sep_mesh.alloc(&self.renderer).draw_untextured();
 
         let item_states = self.item_states.lock();
+        let is_edit_mode = self.is_edit_mode.load(Ordering::Relaxed);
+        let edit_offset = if is_edit_mode { 100.0 } else { 0.0 };
+
+        // Create X mesh for edit mode
+        let x_mesh =
+            if is_edit_mode { Some(shape::make_x(&self.renderer, font_size)) } else { None };
+
+        let mut edit_instrs = vec![];
+        if is_edit_mode {
+            let item_center_y = item_height / 2.0;
+            edit_instrs.push(DrawInstruction::Move(Point::new(handle_padding, item_center_y)));
+            edit_instrs.push(DrawInstruction::Draw(shape::make_x(&self.renderer, font_size)));
+            edit_instrs.push(DrawInstruction::Move(Point::new(-handle_padding, -item_center_y)));
+
+            let rhs = rect.w - handle_padding;
+            edit_instrs.push(DrawInstruction::Move(Point::new(rhs, item_center_y)));
+            edit_instrs.push(DrawInstruction::Draw(shape::make_hammy(&self.renderer, font_size)));
+            edit_instrs.push(DrawInstruction::Move(Point::new(-rhs, -item_center_y)));
+        }
 
         for idx in 0..num_items {
             let item_text = self.items.get_str(idx).unwrap();
@@ -279,6 +336,8 @@ impl Menu {
                 _ => text_color,
             };
 
+            instrs.append(&mut edit_instrs.clone());
+
             // Draw text
             let layout = text::make_layout(
                 &item_text,
@@ -292,9 +351,12 @@ impl Menu {
 
             let text_instr = text::render_layout(&layout, &self.renderer, gfxtag!("menu_text"));
 
-            instrs.push(DrawInstruction::Move(Point::new(padding_x, padding_y)));
+            instrs.push(DrawInstruction::Move(Point::new(padding_x + edit_offset, padding_y)));
             instrs.extend(text_instr);
-            instrs.push(DrawInstruction::Move(Point::new(-padding_x, font_size + padding_y)));
+            instrs.push(DrawInstruction::Move(Point::new(
+                -padding_x - edit_offset,
+                font_size + padding_y,
+            )));
 
             // Draw separator (except for last item)
             if idx < num_items - 1 {
@@ -405,7 +467,7 @@ impl Menu {
 
         if let Some((dt, _)) = info.first_sample() {
             if dt > EPSILON {
-                let velocity = (touch_y - info.start_y) / dt;
+                let velocity = (touch_y - info.start_pos.y) / dt;
                 self.start_scroll(-velocity);
             }
         }
@@ -559,12 +621,30 @@ impl UIObject for Menu {
             return false
         }
 
-        if let Some(item_idx) = self.get_selected_item_index(mouse_pos.y) {
-            self.handle_selection(item_idx).await;
-            true
-        } else {
-            false
+        *self.mouse_click_info.lock() =
+            Some(MouseClickInfo { start_pos: mouse_pos, start_instant: std::time::Instant::now() });
+
+        false
+    }
+
+    async fn handle_mouse_btn_up(&self, btn: MouseButton, mouse_pos: Point) -> bool {
+        if btn != MouseButton::Left {
+            return false
         }
+
+        let click_info = self.mouse_click_info.lock().take();
+        let Some(info) = click_info else { return false };
+
+        let is_click = (mouse_pos.y - info.start_pos.y).abs() < BIG_EPSILON;
+        let movement_dist = ((mouse_pos.x - info.start_pos.x).powi(2) +
+            (mouse_pos.y - info.start_pos.y).powi(2))
+        .sqrt();
+        let is_long_press_tap = movement_dist < LONG_PRESS_EPSILON;
+        let elapsed = info.start_instant.elapsed().as_millis();
+
+        self.handle_interaction(mouse_pos.y, is_click, is_long_press_tap, elapsed).await;
+
+        true
     }
 
     async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
@@ -604,7 +684,7 @@ impl UIObject for Menu {
                 }
 
                 *self.touch_info.lock() =
-                    Some(TouchInfo::new(self.scroll.load(Ordering::Relaxed), touch_pos.y));
+                    Some(TouchInfo::new(self.scroll.load(Ordering::Relaxed), touch_pos));
                 true
             }
 
@@ -622,7 +702,7 @@ impl UIObject for Menu {
                     }
                     info.last_instant = std::time::Instant::now();
 
-                    let dist = touch_pos.y - info.start_y;
+                    let dist = touch_pos.y - info.start_pos.y;
                     if dist.abs() < BIG_EPSILON {
                         return true
                     }
@@ -650,18 +730,20 @@ impl UIObject for Menu {
             TouchPhase::Started | TouchPhase::Moved => false,
 
             TouchPhase::Ended | TouchPhase::Cancelled => {
-                let is_tap = {
+                let (is_tap, is_long_press_tap, elapsed) = {
                     let touch_info = self.touch_info.lock();
                     let Some(info) = &*touch_info else { return true };
 
-                    (touch_pos.y - info.start_y).abs() < BIG_EPSILON
+                    let is_tap = (touch_pos.y - info.start_pos.y).abs() < BIG_EPSILON;
+                    let movement_dist = ((touch_pos.x - info.start_pos.x).powi(2) +
+                        (touch_pos.y - info.start_pos.y).powi(2))
+                    .sqrt();
+                    let is_long_press_tap = movement_dist < LONG_PRESS_EPSILON;
+                    let elapsed = info.start_instant.elapsed().as_millis();
+                    (is_tap, is_long_press_tap, elapsed)
                 };
 
-                if is_tap {
-                    if let Some(item_idx) = self.get_selected_item_index(touch_pos.y) {
-                        self.handle_selection(item_idx).await;
-                    }
-                }
+                self.handle_interaction(touch_pos.y, is_tap, is_long_press_tap, elapsed).await;
 
                 self.end_touch_phase(touch_pos.y);
                 true

+ 108 - 0
bin/app/src/ui/menu/shape.rs

@@ -0,0 +1,108 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2026 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use crate::{
+    gfx::{gfxtag, DrawMesh, Renderer, Vertex},
+    mesh::{Color, MeshBuilder},
+};
+
+const X_COLOR: Color = [1.0, 0.0, 0.0, 1.0];
+
+pub fn make_x(renderer: &Renderer, font_size: f32) -> DrawMesh {
+    let x_size = font_size * 0.8;
+    let half_size = x_size / 2.0;
+    let thickness = 2.0;
+    let half_thick = thickness / 2.0;
+
+    let mut mesh = MeshBuilder::new(gfxtag!("menu_x"));
+
+    // First diagonal line (top-left to bottom-right)
+    // Diagonal from (-half_size, -half_size) to (half_size, half_size)
+    // Normal vector is (1, -1) normalized
+    let mut diag_start = [-half_size - half_thick, -half_size + half_thick];
+    let mut diag_end = [half_size - half_thick, half_size + half_thick];
+    let mut diag_start2 = [-half_size + half_thick, -half_size - half_thick];
+    let mut diag_end2 = [half_size + half_thick, half_size - half_thick];
+
+    let verts1 = vec![
+        Vertex { pos: diag_start, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_start2, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end2, color: X_COLOR, uv: [0., 0.] },
+    ];
+    mesh.append(verts1, vec![0, 2, 1, 1, 2, 3]);
+
+    // Second diagonal line (bottom-left to top-right)
+    // Diagonal from (-half_size, half_size) to (half_size, -half_size)
+    diag_start[0] *= -1.;
+    diag_end[0] *= -1.;
+    diag_start2[0] *= -1.;
+    diag_end2[0] *= -1.;
+
+    let verts2 = vec![
+        Vertex { pos: diag_start, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_start2, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end2, color: X_COLOR, uv: [0., 0.] },
+    ];
+    mesh.append(verts2, vec![0, 2, 1, 1, 2, 3]);
+
+    mesh.alloc(renderer).draw_untextured()
+}
+
+pub fn make_hammy(renderer: &Renderer, font_size: f32) -> DrawMesh {
+    let x_size = font_size * 0.8;
+    let half_size = x_size / 2.0;
+    let thickness = 2.0;
+    let half_thick = thickness / 2.0;
+
+    let mut mesh = MeshBuilder::new(gfxtag!("menu_x"));
+
+    // First diagonal line (top-left to bottom-right)
+    // Diagonal from (-half_size, -half_size) to (half_size, half_size)
+    // Normal vector is (1, -1) normalized
+    let mut diag_start = [-half_size - half_thick, -half_size + half_thick];
+    let mut diag_end = [half_size - half_thick, half_size + half_thick];
+    let mut diag_start2 = [-half_size + half_thick, -half_size - half_thick];
+    let mut diag_end2 = [half_size + half_thick, half_size - half_thick];
+
+    let verts1 = vec![
+        Vertex { pos: diag_start, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_start2, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end2, color: X_COLOR, uv: [0., 0.] },
+    ];
+    mesh.append(verts1, vec![0, 2, 1, 1, 2, 3]);
+
+    // Second diagonal line (bottom-left to top-right)
+    // Diagonal from (-half_size, half_size) to (half_size, -half_size)
+    diag_start[0] *= -1.;
+    diag_end[0] *= -1.;
+    diag_start2[0] *= -1.;
+    diag_end2[0] *= -1.;
+
+    let verts2 = vec![
+        Vertex { pos: diag_start, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_start2, color: X_COLOR, uv: [0., 0.] },
+        Vertex { pos: diag_end2, color: X_COLOR, uv: [0., 0.] },
+    ];
+    mesh.append(verts2, vec![0, 2, 1, 1, 2, 3]);
+
+    mesh.alloc(renderer).draw_untextured()
+}