Przeglądaj źródła

app/menu: smooth scrolling 😎

jkds 6 miesięcy temu
rodzic
commit
bf47853e87
2 zmienionych plików z 100 dodań i 59 usunięć
  1. 3 3
      bin/app/src/app/schema/menu.rs
  2. 97 56
      bin/app/src/ui/menu.rs

+ 3 - 3
bin/app/src/app/schema/menu.rs

@@ -183,9 +183,9 @@ pub async fn make(app: &App, content: SceneNodePtr, i18n_fish: &I18nBabelFish) {
 
     let prop = node.get_property("bg_color").unwrap();
     prop.set_f32(atom, Role::App, 0, 0.).unwrap();
-    prop.set_f32(atom, Role::App, 1, 0.1).unwrap();
-    prop.set_f32(atom, Role::App, 2, 0.07).unwrap();
-    prop.set_f32(atom, Role::App, 3, 0.7).unwrap();
+    prop.set_f32(atom, Role::App, 1, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 2, 0.).unwrap();
+    prop.set_f32(atom, Role::App, 3, 0.5).unwrap();
 
     node.set_property_f32(atom, Role::App, "font_size", CHANNEL_LABEL_FONTSIZE).unwrap();
 

+ 97 - 56
bin/app/src/ui/menu.rs

@@ -34,8 +34,8 @@ use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi},
     mesh::MeshBuilder,
     prop::{
-        BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32,
-        PropertyPtr, PropertyRect, PropertyUint32, Role,
+        BatchGuardId, BatchGuardPtr, PropertyAtomicGuard, PropertyBool, PropertyColor,
+        PropertyFloat32, PropertyPtr, PropertyRect, PropertyUint32, Role,
     },
     scene::{Pimpl, SceneNodeWeak},
     text, ExecutorPtr,
@@ -92,7 +92,8 @@ pub struct Menu {
     node: SceneNodeWeak,
     render_api: RenderApi,
     tasks: SyncMutex<Vec<smol::Task<()>>>,
-    dc_key: u64,
+    root_dc_key: u64,
+    content_dc_key: u64,
 
     is_visible: PropertyBool,
     rect: PropertyRect,
@@ -111,7 +112,6 @@ pub struct Menu {
 
     mouse_pos: SyncMutex<Point>,
     touch_info: SyncMutex<Option<TouchInfo>>,
-    touch_is_active: AtomicBool,
     scroll_start_accel: PropertyFloat32,
     scroll_resist: PropertyFloat32,
     motion_cv: Arc<CondVar>,
@@ -152,7 +152,8 @@ impl Menu {
             node: node.clone(),
             render_api: render_api.clone(),
             tasks: SyncMutex::new(vec![]),
-            dc_key: OsRng.gen(),
+            root_dc_key: OsRng.gen(),
+            content_dc_key: OsRng.gen(),
             is_visible,
             rect,
             scroll,
@@ -168,7 +169,6 @@ impl Menu {
             window_scale,
             mouse_pos: SyncMutex::new(Point::new(0., 0.)),
             touch_info: SyncMutex::new(None),
-            touch_is_active: AtomicBool::new(false),
             scroll_start_accel,
             scroll_resist,
             motion_cv,
@@ -213,7 +213,7 @@ impl Menu {
         self.rect.eval(atom, &parent_rect).ok()?;
         let rect = self.rect.get();
 
-        let mut instrs = vec![DrawInstruction::ApplyView(rect)];
+        let mut instrs = vec![];
 
         let scroll = self.scroll.get();
         let item_height = self.get_item_height();
@@ -227,24 +227,23 @@ impl Menu {
         let window_scale = self.window_scale.get();
 
         let num_items = self.items.get_len();
-        //d!("Number of menu items: {num_items}");
 
-        let start_idx = (scroll / item_height).floor() as usize;
-        let end_idx = ((scroll + rect.h) / item_height).ceil() as usize;
-        let end_idx = end_idx.min(num_items);
+        // Draw single background mesh for the entire menu
+        let content_height = num_items as f32 * item_height;
 
-        for idx in start_idx..end_idx {
-            let item_text = self.items.get_str(idx).unwrap();
-            let y_pos = idx as f32 * item_height - scroll;
+        let mut bg_mesh = MeshBuilder::new(gfxtag!("menu_bg"));
+        bg_mesh.draw_filled_box(&Rectangle::new(0., 0., rect.w, content_height), bg_color);
+        let bg_mesh = bg_mesh.alloc(&self.render_api).draw_untextured();
 
-            // Draw background
-            instrs.push(DrawInstruction::SetPos(Point::new(0., y_pos)));
+        instrs.push(DrawInstruction::Draw(bg_mesh));
 
-            let mut mesh = MeshBuilder::new(gfxtag!("menu_bg"));
-            mesh.draw_filled_box(&Rectangle::new(0., 0., rect.w, item_height), bg_color);
-            let bg_mesh = mesh.alloc(&self.render_api).draw_untextured();
+        // Separator line mesh
+        let mut sep_mesh = MeshBuilder::new(gfxtag!("menu_sep"));
+        sep_mesh.draw_filled_box(&Rectangle::new(0., 0., rect.w, sep_size), sep_color);
+        let sep_mesh = sep_mesh.alloc(&self.render_api).draw_untextured();
 
-            instrs.push(DrawInstruction::Draw(bg_mesh));
+        for idx in 0..num_items {
+            let item_text = self.items.get_str(idx).unwrap();
 
             // Draw text
             let layout = text::make_layout(
@@ -256,33 +255,44 @@ impl Menu {
                 Some(rect.w - padding_x * 2.),
                 &[],
             );
-            //d!("Menu item: {item_text}");
 
             let text_instr = text::render_layout(&layout, &self.render_api, gfxtag!("menu_text"));
 
-            instrs.push(DrawInstruction::SetPos(Point::new(padding_x, y_pos + padding_y)));
+            instrs.push(DrawInstruction::Move(Point::new(padding_x, padding_y)));
             instrs.extend(text_instr);
+            instrs.push(DrawInstruction::Move(Point::new(-padding_x, font_size + padding_y)));
 
             // Draw separator (except for last item)
-            if idx < num_items - 1 && sep_size > 0. {
-                let sep_y = y_pos + item_height - sep_size;
-
-                instrs.push(DrawInstruction::SetPos(Point::new(0., sep_y)));
-
-                let mut mesh = MeshBuilder::new(gfxtag!("menu_sep"));
-                mesh.draw_filled_box(&Rectangle::new(0., 0., rect.w, sep_size), sep_color);
-                let sep_mesh = mesh.alloc(&self.render_api).draw_untextured();
-
-                instrs.push(DrawInstruction::Draw(sep_mesh));
+            if idx < num_items - 1 {
+                instrs.push(DrawInstruction::Draw(sep_mesh.clone()));
             }
         }
 
         Some(DrawUpdate {
-            key: self.dc_key,
-            draw_calls: vec![(
-                self.dc_key,
-                DrawCall::new(instrs, vec![], self.z_index.get(), "menu"),
-            )],
+            key: self.root_dc_key,
+            draw_calls: vec![
+                (
+                    self.root_dc_key,
+                    DrawCall {
+                        instrs: vec![
+                            DrawInstruction::ApplyView(rect),
+                            DrawInstruction::Move(Point::new(0., -scroll)),
+                        ],
+                        dcs: vec![self.content_dc_key],
+                        z_index: self.z_index.get(),
+                        debug_str: "menu_root",
+                    },
+                ),
+                (
+                    self.content_dc_key,
+                    DrawCall {
+                        instrs,
+                        dcs: vec![],
+                        z_index: self.z_index.get(),
+                        debug_str: "menu_content",
+                    },
+                ),
+            ],
         })
     }
 
@@ -295,7 +305,26 @@ impl Menu {
         self.render_api.replace_draw_calls(atom.batch_id, draw_update.draw_calls);
     }
 
-    async fn scrollview(&self, scroll: f32, atom: &mut PropertyAtomicGuard) {
+    fn redraw_scroll(&self, batch_id: BatchGuardId) {
+        let rect = self.rect.get();
+        let scroll = self.scroll.get();
+
+        // Only recreate root with updated scroll position
+        let root_instrs =
+            vec![DrawInstruction::ApplyView(rect), DrawInstruction::Move(Point::new(0., -scroll))];
+
+        let root_dc = DrawCall {
+            instrs: root_instrs,
+            dcs: vec![self.content_dc_key],
+            z_index: self.z_index.get(),
+            debug_str: "menu_root",
+        };
+
+        let draw_calls = vec![(self.root_dc_key, root_dc)];
+        self.render_api.replace_draw_calls(batch_id, draw_calls);
+    }
+
+    fn scrollview(&self, scroll: f32, atom: &mut PropertyAtomicGuard) {
         let item_height = self.get_item_height();
         let num_items = self.items.get_len() as f32;
         let content_height = num_items * item_height;
@@ -303,14 +332,13 @@ impl Menu {
         let rect = self.rect.get();
         let max_scroll = (content_height - rect.h).max(0.);
 
-        let scroll = scroll.clamp(0., max_scroll);
+        // Allow 50% overscroll past the end of the content
+        let overscroll = rect.h * 0.5;
+        let scroll = scroll.clamp(0., max_scroll + overscroll);
         self.scroll.set(atom, scroll);
 
-        if let Some(parent_rect) = self.parent_rect.lock().clone() {
-            if let Some(draw_update) = self.get_draw_calls(atom, parent_rect) {
-                self.render_api.replace_draw_calls(atom.batch_id, draw_update.draw_calls);
-            }
-        }
+        // Only update root draw call with new scroll position
+        self.redraw_scroll(atom.batch_id);
     }
 
     fn start_scroll(&self, delta: f32) {
@@ -334,7 +362,7 @@ impl Menu {
                 let atom = &mut self.render_api.make_guard(gfxtag!("Menu::movement"));
 
                 let scroll = self.scroll.get();
-                self.scrollview(scroll + speed, atom).await;
+                self.scrollview(scroll + speed, atom);
                 speed *= resist;
                 self.speed.store(speed, Ordering::Relaxed);
                 darkfi::system::msleep(16).await;
@@ -345,8 +373,8 @@ impl Menu {
     }
 
     fn end_touch_phase(&self, touch_y: f32) {
-        let touch_info = self.touch_info.lock();
-        let Some(info) = &*touch_info else { return };
+        let touch_info = std::mem::take(&mut *self.touch_info.lock());
+        let info = touch_info.unwrap();
 
         if let Some((dt, _)) = info.first_sample() {
             if dt > EPSILON {
@@ -445,35 +473,33 @@ impl UIObject for Menu {
         false
     }
 
-    async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
+    fn handle_touch_sync(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
         if id != 0 {
             return false
         }
 
-        let rect = self.rect.get();
-
         match phase {
             TouchPhase::Started => {
+                let rect = self.rect.get();
                 if !rect.contains(touch_pos) {
                     *self.touch_info.lock() = None;
                     return false
                 }
 
-                self.touch_is_active.store(true, Ordering::Relaxed);
                 *self.touch_info.lock() = Some(TouchInfo::new(self.scroll.get(), touch_pos.y));
                 true
             }
 
             TouchPhase::Moved => {
-                let (_needs_update, scroll) = {
+                let scroll = {
                     let mut touch_info = self.touch_info.lock();
                     let Some(info) = &mut *touch_info else { return false };
 
                     info.last_y = touch_pos.y;
                     info.push_sample(touch_pos.y);
 
-                    let last_elapsed = info.last_instant.elapsed().as_micros();
-                    if last_elapsed <= 20_000 {
+                    let last_elapsed = info.last_instant.elapsed().as_millis();
+                    if last_elapsed <= 20 {
                         return true
                     }
                     info.last_instant = std::time::Instant::now();
@@ -483,14 +509,28 @@ impl UIObject for Menu {
                         return true
                     }
 
-                    (true, info.start_scroll - dist)
+                    info.start_scroll - dist
                 };
 
                 let atom = &mut self.render_api.make_guard(gfxtag!("Menu::touch"));
-                self.scrollview(scroll, atom).await;
+                self.scrollview(scroll, atom);
                 true
             }
 
+            // Use async handler instead
+            TouchPhase::Ended | TouchPhase::Cancelled => false,
+        }
+    }
+
+    async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
+        if id != 0 {
+            return false
+        }
+
+        match phase {
+            // Should be handled by handle_touch_sync
+            TouchPhase::Started | TouchPhase::Moved => false,
+
             TouchPhase::Ended | TouchPhase::Cancelled => {
                 let is_tap = {
                     let touch_info = self.touch_info.lock();
@@ -500,6 +540,7 @@ impl UIObject for Menu {
                 };
 
                 if is_tap {
+                    let rect = self.rect.get();
                     let click_y = touch_pos.y - rect.y;
 
                     if let Some(item_idx) = self.get_selected_item_index(click_y) {