mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-04-28 03:00:18 -04:00
app/menu: long click hold show buttons to edit entries
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
bin/app/src/ui/menu/shape.rs
Normal file
108
bin/app/src/ui/menu/shape.rs
Normal file
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user