فهرست منبع

app/edit: add an action overlay bar

jkds 7 ماه پیش
والد
کامیت
6f78c1d860
5فایلهای تغییر یافته به همراه304 افزوده شده و 83 حذف شده
  1. 26 0
      bin/app/src/app/node.rs
  2. 2 0
      bin/app/src/app/schema/test.rs
  3. 2 2
      bin/app/src/text/editor/parley.rs
  4. 173 0
      bin/app/src/ui/edit/action.rs
  5. 101 81
      bin/app/src/ui/edit/mod.rs

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

@@ -364,6 +364,32 @@ pub fn create_baseedit(name: &str) -> SceneNode {
     let prop = Property::new("debug", PropertyType::Bool, PropertySubType::Null);
     node.add_property(prop).unwrap();
 
+    let mut prop = Property::new("action_fg_color", PropertyType::Float32, PropertySubType::Color);
+    prop.set_ui_text("Action Menu FG Color", "Foreground color of action menu items");
+    prop.set_array_len(4);
+    prop.set_range_f32(0., 1.);
+    prop.set_defaults_f32(vec![0., 0.94, 1., 1.]).unwrap();
+    node.add_property(prop).unwrap();
+
+    let mut prop = Property::new("action_bg_color", PropertyType::Float32, PropertySubType::Color);
+    prop.set_ui_text("Action Menu BG Color", "Background color of action menu items");
+    prop.set_array_len(4);
+    prop.set_range_f32(0., 1.);
+    prop.set_defaults_f32(vec![0.1, 0.1, 0.1, 0.9]).unwrap();
+    node.add_property(prop).unwrap();
+
+    let mut prop = Property::new("action_padding", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_ui_text("Action Menu Padding", "Padding inside action menu items");
+    prop.set_defaults_f32(vec![8.]).unwrap();
+    prop.set_range_f32(0., f32::MAX);
+    node.add_property(prop).unwrap();
+
+    let mut prop = Property::new("action_spacing", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_ui_text("Action Menu Spacing", "Spacing between action menu items");
+    prop.set_defaults_f32(vec![4.]).unwrap();
+    prop.set_range_f32(0., f32::MAX);
+    node.add_property(prop).unwrap();
+
     node.add_signal("enter_pressed", "Enter key pressed", vec![]).unwrap();
     node.add_signal("focus_request", "Request to gain focus", vec![]).unwrap();
     node.add_signal("paste_request", "Request to show paste dialog", vec![]).unwrap();

+ 2 - 0
bin/app/src/app/schema/test.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+#![allow(unused_imports, unused_variables, dead_code)]
+
 use sled_overlay::sled;
 
 use super::chat::populate_tree;

+ 2 - 2
bin/app/src/text/editor/parley.rs

@@ -96,10 +96,10 @@ impl Editor {
         self.editor.try_layout().unwrap()
     }
 
-    pub fn move_to_pos(&mut self, pos: Point) {
+    pub fn move_to_pos(&mut self, _: Point) {
         unimplemented!()
     }
-    pub fn select_word_at_point(&mut self, _pos: Point) {
+    pub fn select_word_at_point(&mut self, _: Point) {
         unimplemented!()
     }
 

+ 173 - 0
bin/app/src/ui/edit/action.rs

@@ -0,0 +1,173 @@
+/* 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 parking_lot::Mutex as SyncMutex;
+use rand::{rngs::OsRng, Rng};
+
+use crate::{
+    gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle},
+    mesh::{Color, MeshBuilder},
+    prop::BatchGuardId,
+    text, RenderApi,
+};
+
+macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::edit::action", $($arg)*); } }
+
+struct MenuItem {
+    layout: parley::Layout<Color>,
+    action_id: u32,
+    rect: Rectangle,
+}
+
+pub struct Menu {
+    font_size: f32,
+    fg_color: Color,
+    bg_color: Color,
+    padding: f32,
+    spacing: f32,
+    window_scale: f32,
+    pos: Point,
+    items: Vec<MenuItem>,
+}
+
+impl Menu {
+    pub fn new(
+        pos: Point,
+        font_size: f32,
+        fg_color: Color,
+        bg_color: Color,
+        padding: f32,
+        spacing: f32,
+        window_scale: f32,
+    ) -> Self {
+        Self { font_size, fg_color, bg_color, padding, spacing, window_scale, pos, items: vec![] }
+    }
+
+    pub fn add(&mut self, label: &str, action: u32) {
+        let layout = text::make_layout(
+            label,
+            self.fg_color,
+            self.font_size,
+            0.,
+            self.window_scale,
+            None,
+            &vec![],
+        );
+
+        let item_width = layout.width();
+        let item_height = self.font_size + 2. * self.padding;
+
+        let x_offset = match self.items.last() {
+            Some(item) => item.rect.rhs() + self.spacing,
+            None => self.pos.x,
+        };
+
+        let rect = Rectangle::new(
+            x_offset,
+            self.pos.y - item_height,
+            item_width + 2. * self.padding,
+            item_height,
+        );
+
+        self.items.push(MenuItem { layout, action_id: action, rect });
+    }
+}
+
+pub struct ActionMode {
+    pub dc_key: u64,
+
+    menu: SyncMutex<Option<Menu>>,
+    render_api: RenderApi,
+}
+
+impl ActionMode {
+    pub fn new(render_api: RenderApi) -> Self {
+        Self { dc_key: OsRng.gen(), menu: SyncMutex::new(None), render_api }
+    }
+
+    pub fn set(&self, menu: Menu) {
+        *self.menu.lock() = Some(menu);
+    }
+
+    /// Returns `Some(n)` if item n is selected.
+    pub fn interact(&self, mut pos: Point) -> Option<u32> {
+        d!("Interact with action bar {pos:?}");
+        let Some(menu) = std::mem::take(&mut *self.menu.lock()) else {
+            d!("No items");
+            return None
+        };
+
+        // Check each item
+        for item in &menu.items {
+            d!("Checking action item: {:?}", item.rect);
+            if item.rect.contains(pos) {
+                d!("Action clicked: {}", item.action_id);
+                return Some(item.action_id);
+            }
+        }
+
+        // Inside menu but no item clicked
+        d!("Nothing clicked");
+        None
+    }
+
+    /// Called by the parent layout
+    pub fn get_instrs(&self) -> Vec<DrawInstruction> {
+        let Some(menu) = &*self.menu.lock() else { return vec![] };
+
+        let mut instrs = vec![];
+
+        for item in &menu.items {
+            // Used to reset the pos again
+            let mut off_pos = Point::zero();
+
+            // Draw background with border
+            let mut mesh = MeshBuilder::new(gfxtag!("action_bg"));
+            let bg_rect = item.rect.with_zero_pos();
+            mesh.draw_filled_box(&bg_rect, menu.bg_color);
+            mesh.draw_outline(&bg_rect, menu.fg_color, 1.);
+
+            off_pos -= item.rect.pos();
+            instrs.push(DrawInstruction::Move(item.rect.pos()));
+            instrs.push(DrawInstruction::Draw(mesh.alloc(&self.render_api).draw_untextured()));
+
+            // Draw text label
+            let layout_height = item.layout.height();
+            // Center text vertically
+            let text_y = (item.rect.h - layout_height) / 2.;
+            let text_pos = Point::new(menu.padding, text_y);
+            let mut txt_instrs =
+                text::render_layout(&item.layout, &self.render_api, gfxtag!("action_txt"));
+            off_pos -= text_pos;
+            instrs.push(DrawInstruction::Move(text_pos));
+            instrs.append(&mut txt_instrs);
+
+            // Reset cursor
+            instrs.push(DrawInstruction::Move(off_pos));
+        }
+
+        vec![DrawInstruction::Overlay(instrs)]
+    }
+
+    /// When theres a state change, call this to update the draw cmds.
+    pub fn redraw(&self, batch_id: BatchGuardId) {
+        let dcs =
+            vec![(self.dc_key, DrawCall::new(self.get_instrs(), vec![], 1, "chatedit_action"))];
+        self.render_api.replace_draw_calls(batch_id, dcs);
+    }
+}

+ 101 - 81
bin/app/src/ui/edit/mod.rs

@@ -48,8 +48,13 @@ use crate::{
     ExecutorPtr,
 };
 
+const ACTION_COPY: u32 = 0;
+const ACTION_PASTE: u32 = 1;
+const ACTION_SELALL: u32 = 2;
+
 use super::{DrawUpdate, OnModify, UIObject};
 
+mod action;
 mod filter;
 use filter::{ALLOWED_KEYCODES, DISALLOWED_CHARS};
 mod behave;
@@ -81,7 +86,6 @@ enum TouchStateAction {
     Started { pos: Point, instant: std::time::Instant },
     StartSelect,
     Select,
-    Pasta,
     DragSelectHandle { side: isize },
     ScrollVert { start_pos: Point, scroll_start: f32 },
     SetCursorPos,
@@ -138,46 +142,6 @@ impl TouchInfo {
     }
 }
 
-/*
-impl std::fmt::Debug for Editor {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        let mut changes = vec![];
-        let sel = self.editor.raw_selection();
-        if sel.is_collapsed() {
-            let cursor = sel.focus().index();
-            changes.push((cursor, '|'));
-        } else {
-            let sel = sel.text_range();
-            changes.push((sel.start, '{'));
-            changes.push((sel.end, '}'));
-        }
-
-        if let Some(compose) = self.editor.compose() {
-            changes.push((compose.start, '['));
-            changes.push((compose.end, ']'));
-        }
-
-        changes.sort_by(|a, b| b.0.cmp(&a.0));
-
-        write!(f, "'")?;
-        let mut buffer = self.editor.raw_text();
-        for (byte_idx, c) in buffer.char_indices() {
-            while let Some((idx, d)) = changes.last() {
-                if *idx > byte_idx {
-                    break
-                }
-
-                write!(f, "{}", d)?;
-                let _ = changes.pop();
-            }
-
-            write!(f, "{}", c)?;
-        }
-        write!(f, "'")
-    }
-}
-*/
-
 pub type BaseEditPtr = Arc<BaseEdit>;
 
 pub struct BaseEdit {
@@ -194,7 +158,6 @@ pub struct BaseEdit {
     select_dc_key: u64,
     text_dc_key: u64,
     cursor_dc_key: u64,
-    overlay_dc_key: u64,
     cursor_mesh: SyncMutex<Option<DrawMesh>>,
 
     is_active: PropertyBool,
@@ -225,6 +188,11 @@ pub struct BaseEdit {
     priority: PropertyUint32,
     debug: PropertyBool,
 
+    action_fg_color: PropertyColor,
+    action_bg_color: PropertyColor,
+    action_padding: PropertyFloat32,
+    action_spacing: PropertyFloat32,
+
     mouse_btn_held: AtomicBool,
     cursor_is_visible: AtomicBool,
     blink_is_paused: AtomicBool,
@@ -238,7 +206,9 @@ pub struct BaseEdit {
     is_phone_select: AtomicBool,
 
     parent_rect: Arc<SyncMutex<Option<Rectangle>>>,
+    window_scale: PropertyFloat32,
     is_mouse_hover: AtomicBool,
+    action_mode: action::ActionMode,
 
     editor: Arc<SyncMutex<Editor>>,
     behave: Box<dyn EditorBehavior>,
@@ -290,6 +260,15 @@ impl BaseEdit {
         let priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 0).unwrap();
         let debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap();
 
+        let action_fg_color =
+            PropertyColor::wrap(node_ref, Role::Internal, "action_fg_color").unwrap();
+        let action_bg_color =
+            PropertyColor::wrap(node_ref, Role::Internal, "action_bg_color").unwrap();
+        let action_padding =
+            PropertyFloat32::wrap(node_ref, Role::Internal, "action_padding", 0).unwrap();
+        let action_spacing =
+            PropertyFloat32::wrap(node_ref, Role::Internal, "action_spacing", 0).unwrap();
+
         let parent_rect = Arc::new(SyncMutex::new(None));
         let scroll = Arc::new(AtomicF32::new(0.));
 
@@ -297,7 +276,7 @@ impl BaseEdit {
             text.clone(),
             font_size.clone(),
             text_color.clone(),
-            window_scale,
+            window_scale.clone(),
             lineheight.clone(),
         )));
 
@@ -332,6 +311,8 @@ impl BaseEdit {
             }
         };
 
+        let action_mode = action::ActionMode::new(render_api.clone());
+
         let self_ = Arc::new(Self {
             node,
             tasks: SyncMutex::new(vec![]),
@@ -344,7 +325,6 @@ impl BaseEdit {
             select_dc_key: OsRng.gen(),
             text_dc_key: OsRng.gen(),
             cursor_dc_key: OsRng.gen(),
-            overlay_dc_key: OsRng.gen(),
             cursor_mesh: SyncMutex::new(None),
 
             is_active,
@@ -375,6 +355,11 @@ impl BaseEdit {
             priority,
             debug,
 
+            action_fg_color,
+            action_bg_color,
+            action_padding,
+            action_spacing,
+
             mouse_btn_held: AtomicBool::new(false),
             cursor_is_visible: AtomicBool::new(true),
             blink_is_paused: AtomicBool::new(false),
@@ -386,7 +371,9 @@ impl BaseEdit {
             is_phone_select: AtomicBool::new(false),
 
             parent_rect,
+            window_scale,
             is_mouse_hover: AtomicBool::new(false),
+            action_mode,
 
             editor,
             behave,
@@ -782,7 +769,7 @@ impl BaseEdit {
                     self.draw_pasta_overlay(atom.batch_id);
 
                     d!("touch state: StartSelect -> Pasta");
-                    self.touch_info.lock().state = TouchStateAction::Pasta;
+                    self.touch_info.lock().state = TouchStateAction::Inactive;
                     */
                 } else {
                     self.abs_to_local(&mut touch_pos);
@@ -1007,7 +994,6 @@ impl BaseEdit {
                     self.text_dc_key,
                     self.phone_select_handle_dc_key,
                     self.cursor_dc_key,
-                    self.overlay_dc_key,
                     self.select_dc_key,
                 ],
                 0,
@@ -1037,37 +1023,6 @@ impl BaseEdit {
         self.render_api.replace_draw_calls(batch_id, draw_calls);
     }
 
-    fn draw_pasta_overlay(&self, batch_id: BatchGuardId) {
-        let font_size = self.font_size.get();
-        //let window_scale = self.window_scale.get();
-        let lineheight = self.lineheight.get();
-
-        let rect = Rectangle::new(0., 0., 200., 80.);
-        let mut mesh = MeshBuilder::new(gfxtag!("chatedit_overlay_pasta_box"));
-        mesh.draw_box(&rect, [0., 0., 0., 0.2], &Rectangle::zero());
-        mesh.draw_outline(&rect, [1., 0., 0., 1.], 1.);
-        let mut instrs = vec![
-            DrawInstruction::Move(Point::new(0., -100.)),
-            DrawInstruction::Draw(mesh.alloc(&self.render_api).draw_untextured()),
-        ];
-        let layout = text::make_layout(
-            // translator for this
-            "paste",
-            [1., 0., 0., 1.],
-            font_size,
-            lineheight,
-            1.,
-            None,
-            &vec![],
-        );
-        let mut txt_instrs =
-            text::render_layout(&layout, &self.render_api, gfxtag!("chatedit_overlay_pasta_txt"));
-        instrs.append(&mut txt_instrs);
-        let dcs =
-            vec![(self.overlay_dc_key, DrawCall::new(instrs, vec![], 1, "chatedit_overlay_pasta"))];
-        self.render_api.replace_draw_calls(batch_id, dcs);
-    }
-
     fn get_cursor_instrs(&self) -> Vec<DrawInstruction> {
         if !self.is_focused.get() ||
             !self.cursor_is_visible.load(Ordering::Relaxed) ||
@@ -1178,12 +1133,15 @@ impl BaseEdit {
         content_instrs.append(&mut bg_instrs);
         content_instrs.push(DrawInstruction::Move(self.behave.scroll()));
 
+        let action_instrs = self.action_mode.get_instrs();
+
         // + root (move)
         // -+ content (apply view)
         //  └╴select
         //  └╴text
         //  └╴phone_handle
         //  └╴cursor
+        // -- action bar
 
         // Why do we have such a complicated layout?
         // When adjusting selection, it's slow to redraw everything, so the selection
@@ -1197,7 +1155,7 @@ impl BaseEdit {
                     self.root_dc_key,
                     DrawCall::new(
                         vec![DrawInstruction::Move(rect.pos())],
-                        vec![self.content_dc_key, self.overlay_dc_key],
+                        vec![self.content_dc_key, self.action_mode.dc_key],
                         self.z_index.get(),
                         "chatedit_root",
                     ),
@@ -1223,7 +1181,10 @@ impl BaseEdit {
                     self.phone_select_handle_dc_key,
                     DrawCall::new(phone_sel_instrs, vec![], 1, "chatedit_phone_sel"),
                 ),
-                (self.overlay_dc_key, DrawCall::new(vec![], vec![], 0, "chatedit_overlay")),
+                (
+                    self.action_mode.dc_key,
+                    DrawCall::new(action_instrs, vec![], 0, "chatedit_action"),
+                ),
             ],
         }
     }
@@ -1642,11 +1603,33 @@ impl UIObject for BaseEdit {
 
         if btn != MouseButton::Left {
             if btn == MouseButton::Right && rect.contains(mouse_pos) {
+                let cursor_pos = self.get_cursor_pos();
+                let mut menu = action::Menu::new(
+                    cursor_pos,
+                    self.font_size.get(),
+                    self.action_fg_color.get(),
+                    self.action_bg_color.get(),
+                    self.action_padding.get(),
+                    self.action_spacing.get(),
+                    self.window_scale.get(),
+                );
+
                 if self.text.get().is_empty() {
-                    self.node().trigger("paste_request", vec![]).await.unwrap();
+                    //self.node().trigger("paste_request", vec![]).await.unwrap();
+                    menu.add("Paste", ACTION_PASTE);
+                } else {
+                    menu.add("Copy", ACTION_COPY);
+                    menu.add("Paste", ACTION_PASTE);
+                    menu.add("Select All", ACTION_SELALL);
                 }
+                self.action_mode.set(menu);
+                let atom =
+                    &mut self.render_api.make_guard(gfxtag!("BaseEdit::handle_mouse_btn_down"));
+                self.action_mode.redraw(atom.batch_id);
+
                 return true
             }
+
             return false
         }
 
@@ -1683,11 +1666,48 @@ impl UIObject for BaseEdit {
         true
     }
 
-    async fn handle_mouse_btn_up(&self, _btn: MouseButton, _mouse_pos: Point) -> bool {
+    async fn handle_mouse_btn_up(&self, btn: MouseButton, mut mouse_pos: Point) -> bool {
         if !self.is_active.get() {
             return false
         }
 
+        self.abs_to_local(&mut mouse_pos);
+
+        // Check if action was clicked
+        if btn == MouseButton::Left {
+            let atom = &mut self.render_api.make_guard(gfxtag!("BaseEdit::handle_mouse_btn_up"));
+            if let Some(action_id) = self.action_mode.interact(mouse_pos) {
+                match action_id {
+                    ACTION_COPY => {
+                        if let Some(txt) = self.editor.lock().selected_text() {
+                            miniquad::window::clipboard_set(&txt);
+                        }
+                    }
+                    ACTION_PASTE => {
+                        if let Some(txt) = miniquad::window::clipboard_get() {
+                            self.editor.lock().insert(&txt, atom);
+                            self.behave.apply_cursor_scroll();
+                        }
+                    }
+                    ACTION_SELALL => {
+                        self.editor.lock().driver().select_all();
+                        if let Some(seltext) = self.editor.lock().selected_text() {
+                            self.select_text
+                                .clone()
+                                .set_str(atom, Role::Internal, 0, seltext)
+                                .unwrap();
+                        }
+                    }
+                    _ => {}
+                }
+
+                self.redraw(atom);
+                return true;
+            } else {
+                self.action_mode.redraw(atom.batch_id);
+            }
+        }
+
         // Stop any selection scrolling
         let scroll_sender = self.sel_sender.lock().clone().unwrap();
         scroll_sender.send(None).await.unwrap();