Ver Fonte

app: enable proper scrolling while selecting text

darkfi há 10 meses atrás
pai
commit
862288f4d0

+ 1 - 1
bin/app/src/app/node.rs

@@ -226,7 +226,7 @@ pub fn create_text(name: &str) -> SceneNode {
 }
 
 pub fn create_baseedit(name: &str) -> SceneNode {
-    let mut node = SceneNode::new(name, SceneNodeType::ChatEdit);
+    let mut node = SceneNode::new(name, SceneNodeType::Edit);
 
     let mut prop = Property::new("is_active", PropertyType::Bool, PropertySubType::Null);
     prop.set_ui_text("Is Active", "An active EditBox can be focused");

+ 2 - 1
bin/app/src/gfx/linalg.rs

@@ -211,7 +211,7 @@ impl Rectangle {
         Self::new(0., 0., self.w, self.h)
     }
 
-    pub fn clip_point(&self, point: &mut Point) {
+    pub fn clip_point(&self, mut point: Point) -> Point {
         if point.x < self.x {
             point.x = self.x;
         }
@@ -224,6 +224,7 @@ impl Rectangle {
         if point.y > self.y + self.h {
             point.y = self.y + self.h;
         }
+        point
     }
 
     pub fn contains(&self, point: Point) -> bool {

+ 8 - 7
bin/app/src/ui/edit/behave.rs

@@ -41,6 +41,7 @@ pub(super) trait EditorBehavior: Send + Sync {
 
     /// Whenever the cursor is modified this MUST be called
     /// to recalculate the scroll value.
+    /// Must call redraw after this.
     async fn apply_cursor_scroll(&self, atom: &mut PropertyAtomicGuard);
 
     fn scroll(&self) -> Point;
@@ -53,15 +54,15 @@ pub(super) trait EditorBehavior: Send + Sync {
 
     fn allow_endl(&self) -> bool;
 
-    fn finger_scroll_dir(&self) -> FingerScrollDir;
+    fn scroll_ctrl(&self) -> ScrollDir;
 }
 
-pub(super) enum FingerScrollDir {
+pub(super) enum ScrollDir {
     Vert,
     Horiz,
 }
 
-impl FingerScrollDir {
+impl ScrollDir {
     pub fn cmp(&self, grad: f32) -> bool {
         match self {
             Self::Vert => grad.abs() > 0.5,
@@ -230,8 +231,8 @@ impl EditorBehavior for MultiLine {
         true
     }
 
-    fn finger_scroll_dir(&self) -> FingerScrollDir {
-        FingerScrollDir::Vert
+    fn scroll_ctrl(&self) -> ScrollDir {
+        ScrollDir::Vert
     }
 }
 
@@ -321,7 +322,7 @@ impl EditorBehavior for SingleLine {
         false
     }
 
-    fn finger_scroll_dir(&self) -> FingerScrollDir {
-        FingerScrollDir::Horiz
+    fn scroll_ctrl(&self) -> ScrollDir {
+        ScrollDir::Horiz
     }
 }

+ 118 - 36
bin/app/src/ui/edit/mod.rs

@@ -20,6 +20,7 @@ use async_lock::Mutex as AsyncMutex;
 use async_trait::async_trait;
 use darkfi::system::msleep;
 use darkfi_serial::Decodable;
+use futures::{select, FutureExt};
 use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
 use parking_lot::Mutex as SyncMutex;
 use rand::{rngs::OsRng, Rng};
@@ -53,7 +54,7 @@ mod filter;
 use filter::{ALLOWED_KEYCODES, DISALLOWED_CHARS};
 mod behave;
 pub use behave::BaseEditType;
-use behave::{EditorBehavior, FingerScrollDir, MultiLine, SingleLine};
+use behave::{EditorBehavior, MultiLine, ScrollDir, SingleLine};
 mod repeat;
 use repeat::{PressedKey, PressedKeysSmoothRepeat};
 
@@ -66,6 +67,11 @@ const HOLD_ENABLE_TIME: u128 = 500;
 /// Avoid updating too much makes scrolling smoother.
 const VERT_SCROLL_UPDATE_INC: f32 = 1.;
 
+/// How often to update the scrolling selection with mouse.
+const SELECT_TASK_UPDATE_TIME: u64 = 100;
+// Should be a property
+const SELECT_SCROLL_TRAVEL_SPEED: f32 = 1.;
+
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::edit", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::edit", $($arg)*); } }
 
@@ -83,12 +89,12 @@ enum TouchStateAction {
 struct TouchInfo {
     state: TouchStateAction,
     scroll: PropertyFloat32,
-    finger_scroll_dir: FingerScrollDir,
+    scroll_ctrl: ScrollDir,
 }
 
 impl TouchInfo {
-    fn new(scroll: PropertyFloat32, finger_scroll_dir: FingerScrollDir) -> Self {
-        Self { state: TouchStateAction::Inactive, scroll, finger_scroll_dir }
+    fn new(scroll: PropertyFloat32, scroll_ctrl: ScrollDir) -> Self {
+        Self { state: TouchStateAction::Inactive, scroll, scroll_ctrl }
     }
 
     fn start(&mut self, pos: Point) {
@@ -114,7 +120,7 @@ impl TouchInfo {
                         debug!(target: "ui::chatedit::touch", "update touch state: Started -> StartSelect");
                         self.state = TouchStateAction::StartSelect;
                     }
-                } else if self.finger_scroll_dir.cmp(grad) {
+                } else if self.scroll_ctrl.cmp(grad) {
                     // Vertical movement
                     debug!(target: "ui::chatedit::touch", "update touch state: Started -> ScrollVert");
                     let scroll_start = self.scroll.get();
@@ -243,6 +249,8 @@ pub struct BaseEdit {
     blink_is_paused: AtomicBool,
     /// Used to explicitly hide the cursor. Must be manually re-enabled.
     hide_cursor: AtomicBool,
+    /// Used to start select and scroll when mouse moves outside widget rect.
+    sel_sender: SyncMutex<Option<async_channel::Sender<Option<Point>>>>,
 
     touch_info: SyncMutex<TouchInfo>,
     is_phone_select: AtomicBool,
@@ -386,8 +394,9 @@ impl BaseEdit {
             cursor_is_visible: AtomicBool::new(true),
             blink_is_paused: AtomicBool::new(false),
             hide_cursor: AtomicBool::new(false),
+            sel_sender: SyncMutex::new(None),
 
-            touch_info: SyncMutex::new(TouchInfo::new(scroll, behave.finger_scroll_dir())),
+            touch_info: SyncMutex::new(TouchInfo::new(scroll, behave.scroll_ctrl())),
             is_phone_select: AtomicBool::new(false),
 
             window_scale: window_scale.clone(),
@@ -849,7 +858,7 @@ impl BaseEdit {
                 self.redraw_select(atom.batch_id).await;
             }
             TouchStateAction::ScrollVert { start_pos, scroll_start } => {
-                let travel_dist = self.behave.finger_scroll_dir().travel(*start_pos, touch_pos);
+                let travel_dist = self.behave.scroll_ctrl().travel(*start_pos, touch_pos);
                 let mut scroll = scroll_start + travel_dist;
                 scroll = scroll.clamp(0., self.behave.max_scroll().await);
                 if (self.scroll.get() - scroll).abs() < VERT_SCROLL_UPDATE_INC {
@@ -904,6 +913,52 @@ impl BaseEdit {
         self.select_text.clone().set_null(atom, Role::Internal, 0).unwrap();
     }
 
+    async fn handle_select(&self, mouse_pos: Point) {
+        let rect = self.rect.get();
+        let is_mouse_hover = rect.contains(mouse_pos);
+
+        let mut clip_mouse_pos = rect.clip_point(mouse_pos);
+
+        let atom = &mut self.render_api.make_guard(gfxtag!("BaseEdit::handle_mouse_move"));
+
+        // Handle scrolling
+        if !is_mouse_hover {
+            let scroll_ctrl = self.behave.scroll_ctrl();
+            // How far is the cursor outside the widget rect
+            let travel = scroll_ctrl.travel(mouse_pos, clip_mouse_pos);
+            //t!("select autoscroll travel={travel}");
+
+            let max_scroll = self.behave.max_scroll().await;
+            let delta = travel * SELECT_SCROLL_TRAVEL_SPEED;
+            let scroll = (self.scroll.get() + delta).clamp(0., max_scroll);
+            self.scroll.set(atom, scroll);
+
+            self.redraw_scroll(atom.batch_id).await;
+        }
+
+        // Move mouse pos within this widget
+        self.abs_to_local(&mut clip_mouse_pos);
+
+        let seltext = {
+            let mut txt_ctx = text2::TEXT_CTX.get().await;
+            let mut editor = self.lock_editor().await;
+            let mut drv = editor.driver(&mut txt_ctx).unwrap();
+            drv.extend_selection_to_point(clip_mouse_pos.x, clip_mouse_pos.y);
+            editor.selected_text()
+        };
+        d!("Select {seltext:?} from {clip_mouse_pos:?} (unclipped: {mouse_pos:?})");
+
+        // Will be None when drag select just started
+        if let Some(seltext) = seltext {
+            self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
+        }
+
+        self.pause_blinking();
+        //self.behave.apply_cursor_scroll(atom).await;
+        self.redraw_cursor(atom.batch_id).await;
+        self.redraw_select(atom.batch_id).await;
+    }
+
     fn pause_blinking(&self) {
         self.blink_is_paused.store(true, Ordering::Relaxed);
         self.cursor_is_visible.store(true, Ordering::Relaxed);
@@ -1380,7 +1435,45 @@ impl UIObject for BaseEdit {
             }
         });
 
-        let mut tasks = vec![insert_text_task, focus_task, unfocus_task, blinking_cursor_task];
+        let (sel_sender, sel_recvr) = async_channel::unbounded();
+        *self.sel_sender.lock() = Some(sel_sender);
+        let me2 = me.clone();
+        // We don't get continuous mouse move events. Instead this task is used to smoothly
+        // scroll when selecting text.
+        let sel_task = ex.spawn(async move {
+            let mut scroll_stat = None;
+            // Too much code duplication here but I didn't find a solution that looks any cleaner.
+            loop {
+                if scroll_stat.is_some() {
+                    futures::select! {
+                        rcv = sel_recvr.recv().fuse() => {
+                            scroll_stat = rcv.unwrap();
+
+                            if let Some(mouse_pos) = scroll_stat {
+                                let self_ = me2.upgrade().unwrap();
+                                self_.handle_select(mouse_pos).await;
+                            };
+                        }
+                        _ = msleep(SELECT_TASK_UPDATE_TIME).fuse() => {
+                            if let Some(mouse_pos) = scroll_stat {
+                                let self_ = me2.upgrade().unwrap();
+                                self_.handle_select(mouse_pos).await;
+                            };
+                        }
+                    }
+                } else {
+                    scroll_stat = sel_recvr.recv().await.unwrap();
+
+                    if let Some(mouse_pos) = scroll_stat {
+                        let self_ = me2.upgrade().unwrap();
+                        self_.handle_select(mouse_pos).await;
+                    };
+                }
+            }
+        });
+
+        let mut tasks =
+            vec![insert_text_task, focus_task, unfocus_task, blinking_cursor_task, sel_task];
         tasks.append(&mut on_modify.tasks);
 
         #[cfg(target_os = "android")]
@@ -1561,6 +1654,10 @@ impl UIObject for BaseEdit {
             return false
         }
 
+        // Stop any selection scrolling
+        let scroll_sender = self.sel_sender.lock().clone().unwrap();
+        scroll_sender.send(None).await.unwrap();
+
         // releasing mouse button will end selection
         self.mouse_btn_held.store(false, Ordering::Relaxed);
         false
@@ -1572,41 +1669,26 @@ impl UIObject for BaseEdit {
         }
 
         let rect = self.rect.get();
-        self.is_mouse_hover.store(rect.contains(mouse_pos), Ordering::Relaxed);
+        let is_mouse_hover = rect.contains(mouse_pos);
+        self.is_mouse_hover.store(is_mouse_hover, Ordering::Relaxed);
 
         if !self.mouse_btn_held.load(Ordering::Relaxed) {
             return false
         }
 
-        let atom = &mut self.render_api.make_guard(gfxtag!("BaseEdit::handle_mouse_move"));
-
-        // if active and selection_active, then use x to modify the selection.
-        // also implement scrolling when cursor is to the left or right
-        // just scroll to the end
-        // also set cursor_pos too
-
-        // Move mouse pos within this widget
-        self.abs_to_local(&mut mouse_pos);
-
-        let seltext = {
-            let mut txt_ctx = text2::TEXT_CTX.get().await;
-            let mut editor = self.lock_editor().await;
-            let mut drv = editor.driver(&mut txt_ctx).unwrap();
-            drv.extend_selection_to_point(mouse_pos.x, mouse_pos.y);
-            editor.selected_text()
-        };
-        d!("Select {seltext:?} from {mouse_pos:?}");
-
-        // Will be None when drag select just started
-        if let Some(seltext) = seltext {
-            self.select_text.clone().set_str(atom, Role::Internal, 0, seltext).unwrap();
+        let sel_sender = self.sel_sender.lock().clone().unwrap();
+        // Mouse is outside rect?
+        // If so we gotta scroll it while selecting.
+        if !is_mouse_hover {
+            // This process will begin selecting text and applying scroll too.
+            sel_sender.send(Some(mouse_pos)).await.unwrap();
+        } else {
+            // Stop any existing select/scroll process
+            sel_sender.send(None).await.unwrap();
+            // Mouse is inside so just select the text once and be done.
+            self.handle_select(mouse_pos).await;
         }
 
-        self.pause_blinking();
-        self.behave.apply_cursor_scroll(atom).await;
-        self.redraw_scroll(atom.batch_id).await;
-        self.redraw_cursor(atom.batch_id).await;
-        self.redraw_select(atom.batch_id).await;
         true
     }