Эх сурвалжийг харах

wallet: emoji picker fully working

darkfi 1 жил өмнө
parent
commit
c4167744e7

+ 22 - 0
bin/darkwallet/src/app/node.rs

@@ -374,6 +374,9 @@ pub fn create_chatedit(name: &str) -> SceneNode {
 
     node.add_signal("enter_pressed", "Enter key pressed", vec![]).unwrap();
 
+    // Used by emoji_picker
+    node.add_method("insert_text", vec![("text", "Text", CallArgType::Str)], None).unwrap();
+
     node
 }
 
@@ -491,5 +494,24 @@ pub fn create_emoji_picker(name: &str) -> SceneNode {
     let prop = Property::new("z_index", PropertyType::Uint32, PropertySubType::Null);
     node.add_property(prop).unwrap();
 
+    let mut prop = Property::new("scroll", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_ui_text("Scroll", "Scroll down from the top");
+    prop.set_range_f32(0., f32::MAX);
+    node.add_property(prop).unwrap();
+
+    let mut prop =
+        Property::new("mouse_scroll_speed", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_ui_text("Mouse Scroll Speed", "Mouse Scrolling speed");
+    prop.set_defaults_f32(vec![4.]).unwrap();
+    node.add_property(prop).unwrap();
+
+    let mut prop = Property::new("emoji_size", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_ui_text("Emoji Size", "The emoji's size");
+    prop.set_range_f32(0., f32::MAX);
+    node.add_property(prop).unwrap();
+
+    node.add_signal("emoji_select", "Emoji selected", vec![("text", "Text", CallArgType::Str)])
+        .unwrap();
+
     node
 }

+ 51 - 10
bin/darkwallet/src/app/schema/chat.rs

@@ -38,8 +38,8 @@ use crate::{
     shape,
     text::TextShaperPtr,
     ui::{
-        Button, ChatEdit, ChatView, EditBox, EmojiPicker, Image, Layer, ShapeVertex, Text,
-        VectorArt, VectorShape, Window,
+        emoji_picker, Button, ChatEdit, ChatView, EditBox, EmojiPicker, Image, Layer, ShapeVertex,
+        Text, VectorArt, VectorShape, Window,
     },
     ExecutorPtr,
 };
@@ -142,9 +142,16 @@ mod ui_consts {
     pub const CHATVIEW_BASELINE: f32 = 20.;
 }
 
+use super::EMOJI_PICKER_ICON_SIZE;
 use ui_consts::*;
 
-pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db) {
+pub async fn make(
+    app: &App,
+    window: SceneNodePtr,
+    channel: &str,
+    db: &sled::Db,
+    emoji_meshes: emoji_picker::EmojiMeshesPtr,
+) {
     let window_scale = PropertyFloat32::wrap(&window, Role::Internal, "scale", 0).unwrap();
 
     let mut cc = Compiler::new();
@@ -289,18 +296,19 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
     let mut node = create_emoji_picker("emoji_picker");
     let prop = Property::new("dynamic_h", PropertyType::Float32, PropertySubType::Pixel);
     node.add_property(prop).unwrap();
-    let emoji_h_prop = node.get_property("dynamic_h").unwrap();
+    let emoji_dynamic_h_prop = node.get_property("dynamic_h").unwrap();
+    //emoji_dynamic_h_prop.set_f32(Role::App, 0, 400.).unwrap();
     let prop = node.get_property("rect").unwrap();
     prop.set_f32(Role::App, 0, 0.).unwrap();
     let code = cc.compile("h - dynamic_h").unwrap();
     prop.set_expr(Role::App, 1, code).unwrap();
     prop.set_expr(Role::App, 2, expr::load_var("w")).unwrap();
     prop.set_expr(Role::App, 3, expr::load_var("dynamic_h")).unwrap();
-    prop.add_depend(&emoji_h_prop, 0, "dynamic_h");
+    prop.add_depend(&emoji_dynamic_h_prop, 0, "dynamic_h");
     let emoji_h_prop = PropertyFloat32::wrap(&node, Role::App, "dynamic_h", 0).unwrap();
-    let emoji_rect_prop = prop;
     //node.set_property_f32(Role::App, "baseline", CHANNEL_LABEL_BASELINE).unwrap();
     //node.set_property_f32(Role::App, "font_size", FONTSIZE).unwrap();
+    node.set_property_f32(Role::App, "emoji_size", EMOJI_PICKER_ICON_SIZE).unwrap();
     node.set_property_u32(Role::App, "z_index", 2).unwrap();
     let node = node
         .setup(|me| {
@@ -308,11 +316,12 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
                 me,
                 window_scale.clone(),
                 app.render_api.clone(),
-                app.text_shaper.clone(),
+                emoji_meshes,
                 app.ex.clone(),
             )
         })
         .await;
+    let emoji_picker_node = node.clone();
     layer_node.clone().link(node);
 
     // Main content view
@@ -324,7 +333,7 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
     prop.set_expr(Role::App, 2, expr::load_var("w")).unwrap();
     let code = cc.compile("h - emoji_h").unwrap();
     prop.set_expr(Role::App, 3, code).unwrap();
-    prop.add_depend(&emoji_rect_prop, 3, "emoji_h");
+    prop.add_depend(&emoji_dynamic_h_prop, 0, "emoji_h");
     layer_node.set_property_bool(Role::App, "is_visible", true).unwrap();
     layer_node.set_property_u32(Role::App, "z_index", 1).unwrap();
     let layer_node =
@@ -457,7 +466,20 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
         .unwrap();
     prop.set_expr(Role::App, 1, code).unwrap();
     prop.set_expr(Role::App, 2, expr::load_var("w")).unwrap();
-    prop.set_f32(Role::App, 3, 500.).unwrap();
+    let code = cc
+        .compile(
+            "
+        height = if editz_h < CHATEDIT_MIN_HEIGHT {
+            CHATEDIT_MIN_HEIGHT
+        } else {
+            editz_h
+        };
+
+        height + 2 * CHATEDIT_BOTTOM_PAD
+",
+        )
+        .unwrap();
+    prop.set_expr(Role::App, 3, code).unwrap();
     node.set_property_u32(Role::App, "z_index", 2).unwrap();
 
     let editbox_bg_rect_prop = prop.clone();
@@ -467,7 +489,7 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
     shape.add_filled_box(
         expr::const_f32(0.),
         expr::const_f32(0.),
-        cc.compile("w").unwrap(),
+        expr::load_var("w"),
         expr::load_var("h"),
         [0., 0.13, 0.08, 1.],
     );
@@ -495,6 +517,14 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
         expr::load_var("h"),
         [0.41, 0.6, 0.65, 1.],
     );
+    // Bottom line
+    shape.add_filled_box(
+        expr::const_f32(0.),
+        cc.compile("h - 1").unwrap(),
+        expr::load_var("w"),
+        expr::load_var("h"),
+        [0.41, 0.6, 0.65, 1.],
+    );
     let node =
         node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
     layer_node.clone().link(node);
@@ -660,8 +690,19 @@ pub async fn make(app: &App, window: SceneNodePtr, channel: &str, db: &sled::Db)
             )
         })
         .await;
+    let chatedit_node = node.clone();
     layer_node.clone().link(node);
 
+    let (slot, recvr) = Slot::new("emoji_selected");
+    emoji_picker_node.register("emoji_select", slot).unwrap();
+    let listen_click = app.ex.spawn(async move {
+        while let Ok(data) = recvr.recv().await {
+            // No need to decode the data. Just pass it straight along
+            chatedit_node.call_method("insert_text", data).await.unwrap();
+        }
+    });
+    app.tasks.lock().unwrap().push(listen_click);
+
     // Create the send button
     let node = create_button("send_btn");
     node.set_property_bool(Role::App, "is_active", true).unwrap();

+ 11 - 8
bin/darkwallet/src/app/schema/mod.rs

@@ -37,8 +37,8 @@ use crate::{
     shape,
     text::TextShaperPtr,
     ui::{
-        Button, ChatEdit, ChatView, EditBox, Image, Layer, ShapeVertex, Text, VectorArt,
-        VectorShape, Window,
+        emoji_picker, Button, ChatEdit, ChatView, EditBox, Image, Layer, ShapeVertex, Text,
+        VectorArt, VectorShape, Window,
     },
     ExecutorPtr,
 };
@@ -51,6 +51,7 @@ mod menu;
 mod ui_consts {
     pub const CHATDB_PATH: &str = "/data/data/darkfi.darkwallet/chatdb/";
     pub const BG_PATH: &str = "bg.png";
+    pub const EMOJI_PICKER_ICON_SIZE: f32 = 100.;
 }
 
 #[cfg(all(
@@ -60,6 +61,7 @@ mod ui_consts {
 mod ui_consts {
     pub const CHATDB_PATH: &str = "chatdb";
     pub const BG_PATH: &str = "assets/bg.png";
+    pub const EMOJI_PICKER_ICON_SIZE: f32 = 40.;
 }
 
 use ui_consts::*;
@@ -150,14 +152,15 @@ pub async fn make(app: &App, window: SceneNodePtr) {
         node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await;
     layer_node.clone().link(node);
 
+    let emoji_meshes = emoji_picker::EmojiMeshes::new(
+        app.render_api.clone(),
+        app.text_shaper.clone(),
+        EMOJI_PICKER_ICON_SIZE,
+    );
+
     let db = sled::open(CHATDB_PATH).expect("cannot open sleddb");
     for channel in CHANNELS {
-        chat::make(app, window.clone(), channel, &db).await;
+        chat::make(app, window.clone(), channel, &db, emoji_meshes.clone()).await;
     }
     menu::make(app, window.clone()).await;
-
-    let chatview_node = app.sg_root.clone().lookup_node("/window/dev_chat_layer").unwrap();
-    chatview_node.set_property_bool(Role::App, "is_visible", true).unwrap();
-    let menu_node = app.sg_root.clone().lookup_node("/window/menu_layer").unwrap();
-    menu_node.set_property_bool(Role::App, "is_visible", false).unwrap();
 }

+ 57 - 18
bin/darkwallet/src/ui/chatedit.rs

@@ -20,11 +20,13 @@ use async_lock::Mutex as AsyncMutex;
 use async_trait::async_trait;
 use atomic_float::AtomicF32;
 use darkfi::system::msleep;
+use darkfi_serial::{deserialize, Decodable, Encodable, SerialDecodable, SerialEncodable};
 use miniquad::{window, KeyCode, KeyMods, MouseButton, TouchPhase};
 use parking_lot::Mutex as SyncMutex;
 use rand::{rngs::OsRng, Rng};
 use std::{
     collections::HashMap,
+    io::Cursor,
     sync::{
         atomic::{AtomicBool, Ordering},
         Arc, OnceLock, Weak,
@@ -44,7 +46,7 @@ use crate::{
         PropertyUint32, Role,
     },
     pubsub::Subscription,
-    scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
+    scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
     text::{self, Glyph, GlyphPositionIter, TextShaperPtr},
     util::{enumerate_ref, is_whitespace, min_f32, zip3},
     ExecutorPtr,
@@ -1026,15 +1028,23 @@ impl ChatEdit {
 
     async fn insert_char(&self, key: char) {
         debug!(target: "ui::chatedit", "insert_char({key})");
+        let mut tmp = [0; 4];
+        let key_str = key.encode_utf8(&mut tmp);
+        self.insert_text(key_str).await
+    }
+    async fn insert_text(&self, text: &str) {
+        //debug!(target: "ui::chatedit", "insert_text({text})");
         {
             let mut text_wrap = &mut self.text_wrap.lock();
             text_wrap.clear_cache();
             if !text_wrap.select.is_empty() {
                 text_wrap.delete_selected();
+
+                self.is_phone_select.store(false, Ordering::Relaxed);
+                // Reshow cursor (if hidden)
+                self.hide_cursor.store(false, Ordering::Relaxed);
             }
-            let mut tmp = [0; 4];
-            let key_str = key.encode_utf8(&mut tmp);
-            text_wrap.editable.compose(key_str, true);
+            text_wrap.editable.compose(text, true);
         }
 
         self.pause_blinking();
@@ -1637,6 +1647,35 @@ impl ChatEdit {
             ],
         })
     }
+
+    async fn process_insert_text_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
+        let Ok(method_call) = sub.receive().await else {
+            debug!(target: "ui::chatedit", "Event relayer closed");
+            return false
+        };
+
+        //debug!(target: "ui::chatview", "method called: insert_line({method_call:?})");
+        assert!(method_call.send_res.is_none());
+
+        fn decode_data(data: &[u8]) -> std::io::Result<String> {
+            let mut cur = Cursor::new(&data);
+            let text = String::decode(&mut cur)?;
+            Ok(text)
+        }
+
+        let Ok(text) = decode_data(&method_call.data) else {
+            error!(target: "ui::chatedit", "insert_text() method invalid arg data");
+            return true
+        };
+
+        let Some(self_) = me.upgrade() else {
+            // Should not happen
+            panic!("self destroyed before insert_text_method_task was stopped!");
+        };
+
+        self_.insert_text(&text).await;
+        true
+    }
 }
 
 impl Drop for ChatEdit {
@@ -1658,6 +1697,13 @@ impl UIObject for ChatEdit {
         let node_name = node_ref.name.clone();
         let node_id = node_ref.id;
 
+        let method_sub = node_ref.subscribe_method_call("insert_text").unwrap();
+        let me2 = me.clone();
+        let insert_text_task =
+            ex.spawn(
+                async move { while Self::process_insert_text_method(&me2, &method_sub).await {} },
+            );
+
         let mut on_modify = OnModify::new(ex.clone(), node_name, node_id, me.clone());
         on_modify.when_change(self.is_focused.prop(), Self::change_focus);
 
@@ -1726,8 +1772,8 @@ impl UIObject for ChatEdit {
             }
         });
 
-        let mut tasks = on_modify.tasks;
-        tasks.push(blinking_cursor_task);
+        let mut tasks = vec![insert_text_task, blinking_cursor_task];
+        tasks.append(&mut on_modify.tasks);
         self.tasks.set(tasks);
     }
 
@@ -1802,24 +1848,17 @@ impl UIObject for ChatEdit {
 
         let rect = self.rect.get();
 
-        // clicking inside box will:
-        // 1. make it active
-        // 2. begin selection
         if !rect.contains(mouse_pos) {
-            if self.is_focused.get() {
-                debug!(target: "ui::chatedit", "EditBox unfocused");
-                //self.is_focused.set(false);
-                self.text_wrap.lock().select.clear();
-
-                self.redraw().await;
-            }
             return false
         }
 
+        // clicking inside box will:
+        // 1. make it active
+        // 2. begin selection
         if self.is_focused.get() {
-            debug!(target: "ui::chatedit", "EditBox clicked");
+            debug!(target: "ui::chatedit", "ChatEdit clicked");
         } else {
-            debug!(target: "ui::chatedit", "EditBox focused");
+            debug!(target: "ui::chatedit", "ChatEdit focused");
             self.is_focused.set(true);
         }
 

+ 2 - 2
bin/darkwallet/src/ui/chatview/mod.rs

@@ -300,7 +300,7 @@ impl ChatView {
 
         let Some(self_) = me.upgrade() else {
             // Should not happen
-            panic!("self destroyed before touch_task was stopped!");
+            panic!("self destroyed before insert_line_method_task was stopped!");
         };
 
         self_.handle_insert_line(timestamp, msg_id, nick, text).await;
@@ -557,7 +557,7 @@ impl ChatView {
             let msg_id = MessageId(k[8..].try_into().unwrap());
             let timest = Timestamp::from_be_bytes(timest_bytes);
             let chatmsg: ChatMsg = deserialize(&v).unwrap();
-            debug!(target: "ui::chatview", "{timest:?} {chatmsg:?}");
+            //debug!(target: "ui::chatview", "{timest:?} {chatmsg:?}");
 
             let msg_height = msgbuf.push_privmsg(timest, msg_id, chatmsg.nick, chatmsg.text);
 

+ 1 - 1
bin/darkwallet/src/ui/chatview/page.rs

@@ -846,7 +846,7 @@ impl MessageBuffer {
         nick: String,
         text: String,
     ) -> f32 {
-        debug!(target: "ui::chatview", "MessageBuffer::push_privmsg({timest}, {msg_id}, {nick}, {text})");
+        //debug!(target: "ui::chatview", "MessageBuffer::push_privmsg({timest}, {msg_id}, {nick}, {text})");
         let font_size = self.font_size.get();
         let timestamp_font_size = self.timestamp_font_size.get();
         let timestamp_width = self.timestamp_width.get();

+ 0 - 188
bin/darkwallet/src/ui/emoji_picker.rs

@@ -1,188 +0,0 @@
-/* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2024 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 async_trait::async_trait;
-use image::ImageReader;
-use rand::{rngs::OsRng, Rng};
-use std::{
-    io::Cursor,
-    sync::{Arc, Mutex as SyncMutex, OnceLock, Weak},
-};
-
-use crate::{
-    gfx::{
-        GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, GfxTextureId, ManagedTexturePtr, Rectangle,
-        RenderApi,
-    },
-    mesh::{MeshBuilder, MeshInfo, COLOR_WHITE},
-    prop::{PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role},
-    scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
-    text::{self, GlyphPositionIter, TextShaper, TextShaperPtr},
-    ExecutorPtr,
-};
-
-use super::{DrawUpdate, OnModify, UIObject};
-
-macro_rules! d {
-    ($($arg:tt)*) => {
-        debug!(target: "ui::emoji_picker", $($arg)*);
-    }
-}
-
-pub type EmojiPickerPtr = Arc<EmojiPicker>;
-
-pub struct EmojiPicker {
-    node: SceneNodeWeak,
-    render_api: RenderApi,
-    text_shaper: TextShaperPtr,
-    tasks: OnceLock<Vec<smol::Task<()>>>,
-
-    dc_key: u64,
-
-    rect: PropertyRect,
-    z_index: PropertyUint32,
-
-    window_scale: PropertyFloat32,
-    parent_rect: SyncMutex<Option<Rectangle>>,
-}
-
-impl EmojiPicker {
-    pub async fn new(
-        node: SceneNodeWeak,
-        window_scale: PropertyFloat32,
-        render_api: RenderApi,
-        text_shaper: TextShaperPtr,
-        ex: ExecutorPtr,
-    ) -> Pimpl {
-        d!("EmojiPicker::new()");
-
-        let node_ref = &node.upgrade().unwrap();
-        let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
-        let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
-
-        let node_name = node_ref.name.clone();
-        let node_id = node_ref.id;
-
-        let self_ = Arc::new(Self {
-            node,
-            render_api,
-            text_shaper,
-            tasks: OnceLock::new(),
-
-            dc_key: OsRng.gen(),
-
-            rect,
-            z_index,
-
-            window_scale,
-            parent_rect: SyncMutex::new(None),
-        });
-
-        Pimpl::EmojiPicker(self_)
-    }
-
-    async fn redraw(self: Arc<Self>) {
-        let Some(parent_rect) = self.parent_rect.lock().unwrap().clone() else { return };
-
-        let Some(draw_update) = self.get_draw_calls(parent_rect).await else {
-            error!(target: "ui::image", "Emoji picker failed to draw");
-            return;
-        };
-        self.render_api.replace_draw_calls(draw_update.draw_calls);
-        debug!(target: "ui::image", "replace draw calls done");
-    }
-
-    /*
-    fn regen_mesh(&self) -> MeshInfo {
-        let rect = self.rect.get();
-        let uv = self.uv.get();
-        let mesh_rect = Rectangle::from([0., 0., rect.w, rect.h]);
-        let mut mesh = MeshBuilder::new();
-        mesh.draw_box(&mesh_rect, COLOR_WHITE, &uv);
-        mesh.alloc(&self.render_api)
-    }
-    */
-
-    async fn get_draw_calls(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
-        if let Err(e) = self.rect.eval(&parent_rect) {
-            warn!(target: "ui::emoji_picker", "Rect eval failed: {e}");
-            return None
-        }
-        let rect = self.rect.get();
-
-        /*
-        let mesh = self.regen_mesh();
-        let texture = self.texture.lock().unwrap().clone().expect("Node missing texture_id!");
-
-        let mesh = GfxDrawMesh {
-            vertex_buffer: mesh.vertex_buffer,
-            index_buffer: mesh.index_buffer,
-            texture: Some(texture),
-            num_elements: mesh.num_elements,
-        };
-        */
-
-        Some(DrawUpdate {
-            key: self.dc_key,
-            draw_calls: vec![(
-                self.dc_key,
-                GfxDrawCall {
-                    instrs: vec![
-                        GfxDrawInstruction::Move(rect.pos()),
-                        //GfxDrawInstruction::Draw(mesh),
-                    ],
-                    dcs: vec![],
-                    z_index: self.z_index.get(),
-                },
-            )],
-        })
-    }
-}
-
-#[async_trait]
-impl UIObject for EmojiPicker {
-    fn z_index(&self) -> u32 {
-        self.z_index.get()
-    }
-
-    async fn start(self: Arc<Self>, ex: ExecutorPtr) {
-        let me = Arc::downgrade(&self);
-
-        let node_ref = &self.node.upgrade().unwrap();
-        let node_name = node_ref.name.clone();
-        let node_id = node_ref.id;
-
-        let mut on_modify = OnModify::new(ex, node_name, node_id, me.clone());
-        on_modify.when_change(self.rect.prop(), Self::redraw);
-        on_modify.when_change(self.z_index.prop(), Self::redraw);
-
-        self.tasks.set(on_modify.tasks);
-    }
-
-    async fn draw(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
-        debug!(target: "ui::image", "Image::draw()");
-        *self.parent_rect.lock().unwrap() = Some(parent_rect);
-        self.get_draw_calls(parent_rect).await
-    }
-}
-
-impl Drop for EmojiPicker {
-    fn drop(&mut self) {
-        self.render_api.replace_draw_calls(vec![(self.dc_key, Default::default())]);
-    }
-}

+ 38 - 0
bin/darkwallet/src/ui/emoji_picker/emoji.rs

@@ -0,0 +1,38 @@
+pub static EMOJI_LIST: &[&str] = &[
+    "😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "🫠", "😉", "😊", "😇", "🥰", "😍",
+    "🤩", "😘", "😗", "😚", "😙", "🥲", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🫢", "🫣",
+    "🤫", "🤔", "🫡", "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥", "🫨", "😌", "😔",
+    "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮", "🤧", "🥵", "🥶", "🥴", "😵", "🤯", "🤠", "🥳",
+    "🥸", "😎", "🤓", "🧐", "😕", "🫤", "😟", "🙁", "😮", "😯", "😲", "😳", "🥺", "🥹", "😦", "😧",
+    "😨", "😰", "😥", "😢", "😭", "😱", "😖", "😣", "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠",
+    "🤬", "😈", "👿", "💀", "💩", "🤡", "👹", "👺", "👻", "👽", "👾", "🤖", "😺", "😸", "😹", "😻",
+    "😼", "😽", "🙀", "😿", "😾", "🙈", "🙉", "🙊", "💘", "💝", "💖", "💗", "💓", "💞", "💕", "💔",
+    "🩷", "🧡", "💛", "💚", "💙", "🩵", "💜", "🤎", "🖤", "🩶", "🤍", "💋", "💯", "💢", "💥", "💫",
+    "💦", "💨", "💬", "🗨", "🗯", "💭", "💤", "👋", "🤚", "✋", "🖖", "🫱", "🫲", "🫳", "🫴", "🫷",
+    "🫸", "👌", "🤌", "🤏", "🤞", "🫰", "🤟", "🤘", "🤙", "👈", "👉", "👆", "🖕", "👇", "🫵", "👍",
+    "👎", "✊", "👊", "🤛", "🤜", "👏", "🙌", "🫶", "👐", "🤲", "🤝", "🙏", "💅", "🤳", "💪", "🦾",
+    "🦿", "🦵", "🦶", "👂", "🦻", "👃", "🧠", "🫀", "🫁", "🦷", "🦴", "👀", "👁", "👅", "👄", "🫦",
+    "👶", "🧒", "👦", "👧", "🧑", "👱", "👨", "🧔", "👩", "🧓", "👴", "👵", "🙍", "🙎", "🙅", "🙆",
+    "💁", "🙋", "🧏", "🙇", "🤦", "🤷", "👮", "💂", "🥷", "👷", "🫅", "🤴", "👸", "👳", "👲", "🧕",
+    "🤵", "👰", "🤰", "🫃", "🫄", "🤱", "👼", "🎅", "🤶", "🦸", "🦹", "🧙", "🧚", "🧛", "🧜", "🧝",
+    "🧞", "🧟", "🧌", "💆", "💇", "🚶", "🧍", "🧎", "🏃", "💃", "🕺", "🕴", "👯", "🧖", "🧗", "🤺",
+    "🏇", "⛷", "🏂", "🏌", "🏄", "🚣", "🏊", "⛹", "🏋", "🚴", "🚵", "🤸", "🤼", "🤽", "🤾", "🤹",
+    "🧘", "🛀", "🛌", "👭", "👫", "👬", "💏", "💑", "🗣", "👤", "👥", "🫂", "👪", "👣", "🐵", "🐒",
+    "🦍", "🦧", "🐶", "🐕", "🦮", "🐩", "🐺", "🦊", "🦝", "🐱", "🐈", "🦁", "🐯", "🐅", "🐆", "🐴",
+    "🫎", "🫏", "🐎", "🦄", "🦓", "🦌", "🦬", "🐮", "🐂", "🐃", "🐄", "🐷", "🐖", "🐗", "🐽", "🐏",
+    "🐑", "🐐", "🐪", "🐫", "🦙", "🦒", "🐘", "🦣", "🦏", "🦛", "🐭", "🐁", "🐀", "🐹", "🐰", "🐇",
+    "🐿", "🦫", "🦔", "🦇", "🐻", "🐨", "🐼", "🦥", "🦦", "🦨", "🦘", "🦡", "🐾", "🦃", "🐔", "🐓",
+    "🐣", "🐤", "🐥", "🐦", "🐧", "🕊", "🦅", "🦆", "🦢", "🦉", "🦤", "🪶", "🦩", "🦚", "🦜", "🪽",
+    "🪿", "🐸", "🐊", "🐢", "🦎", "🐍", "🐲", "🐉", "🦕", "🦖", "🐳", "🐋", "🐬", "🦭", "🐟", "🐠",
+    "🐡", "🦈", "🐙", "🐚", "🪸", "🪼", "🦀", "🦞", "🦐", "🦑", "🦪", "🐌", "🦋", "🐛", "🐜", "🐝",
+    "🪲", "🐞", "🦗", "🪳", "🕷", "🕸", "🦂", "🦟", "🪰", "🪱", "🦠", "💐", "🌸", "💮", "🪷", "🏵",
+    "🌹", "🥀", "🌺", "🌻", "🌼", "🌷", "🪻", "🌱", "🪴", "🌲", "🌳", "🌴", "🌵", "🌾", "🌿", "☘",
+    "🍀", "🍁", "🍂", "🍃", "🪹", "🪺", "🍄", "🍇", "🍈", "🍉", "🍊", "🍋", "🍌", "🍍", "🥭", "🍎",
+    "🍏", "🍐", "🍑", "🍒", "🍓", "🫐", "🥝", "🍅", "🫒", "🥥", "🥑", "🍆", "🥔", "🥕", "🌽", "🌶",
+    "🫑", "🥒", "🥬", "🥦", "🧄", "🧅", "🥜", "🫘", "🌰", "🫚", "🫛", "🍞", "🥐", "🥖", "🫓", "🥨",
+    "🥯", "🥞", "🧇", "🧀", "🍖", "🍗", "🥩", "🥓", "🍔", "🍟", "🍕", "🌭", "🥪", "🌮", "🌯", "🫔",
+    "🥙", "🧆", "🥚", "🍳", "🥘", "🍲", "🫕", "🥣", "🥗", "🍿", "🧈", "🧂", "🥫", "🍱", "🍘", "🍙",
+    "🍚", "🍛", "🍜", "🍝", "🍠", "🍢", "🍣", "🍤", "🍥", "🥮", "🍡", "🥟", "🥠", "🥡", "🍦", "🍧",
+    "🍨", "🍩", "🍪", "🎂", "🍰", "🧁", "🥧", "🍫", "🍬", "🍭", "🍮", "🍯", "🍼", "🥛", "☕", "🫖",
+    "🍵", "🍶",
+];

+ 430 - 0
bin/darkwallet/src/ui/emoji_picker/mod.rs

@@ -0,0 +1,430 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 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 async_trait::async_trait;
+use darkfi_serial::Encodable;
+use image::ImageReader;
+use miniquad::{MouseButton, TouchPhase};
+use rand::{rngs::OsRng, Rng};
+use std::{
+    io::Cursor,
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc, Mutex as SyncMutex, OnceLock, Weak,
+    },
+};
+
+use crate::{
+    gfx::{
+        GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, GfxTextureId, ManagedTexturePtr, Point,
+        Rectangle, RenderApi,
+    },
+    mesh::{MeshBuilder, MeshInfo, COLOR_WHITE},
+    prop::{PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr, PropertyUint32, Role},
+    scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
+    text::{self, GlyphPositionIter, TextShaper, TextShaperPtr},
+    ExecutorPtr,
+};
+
+use super::{DrawUpdate, OnModify, UIObject};
+
+mod emoji;
+
+macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::emoji_picker", $($arg)*); } }
+
+pub type EmojiMeshesPtr = Arc<SyncMutex<EmojiMeshes>>;
+
+pub struct EmojiMeshes {
+    render_api: RenderApi,
+    text_shaper: TextShaperPtr,
+    emoji_size: f32,
+    meshes: Vec<GfxDrawMesh>,
+}
+
+impl EmojiMeshes {
+    pub fn new(
+        render_api: RenderApi,
+        text_shaper: TextShaperPtr,
+        emoji_size: f32,
+    ) -> EmojiMeshesPtr {
+        Arc::new(SyncMutex::new(Self {
+            render_api,
+            text_shaper,
+            emoji_size,
+            meshes: Vec::with_capacity(emoji::EMOJI_LIST.len()),
+        }))
+    }
+
+    /// Make mesh for this emoji centered at (0, 0)
+    fn gen_emoji_mesh(&self, emoji: &str) -> GfxDrawMesh {
+        //d!("rendering emoji: '{emoji}'");
+        // The params here don't actually matter since we're talking about BMP fixed sizes
+        let glyphs = self.text_shaper.shape(emoji.to_string(), 10., 1.);
+        assert_eq!(glyphs.len(), 1);
+        let atlas = text::make_texture_atlas(&self.render_api, &glyphs);
+        let glyph = glyphs.into_iter().next().unwrap();
+
+        // Emoji's vary in size. We make them all a consistent size.
+        let w = self.emoji_size;
+        let h =
+            (glyph.sprite.bmp_height as f32) * self.emoji_size / (glyph.sprite.bmp_width as f32);
+        // Center at origin
+        let x = -w / 2.;
+        let y = -h / 2.;
+
+        let uv = atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
+        let mut mesh = MeshBuilder::new();
+        mesh.draw_box(&Rectangle::new(x, y, w, h), COLOR_WHITE, &uv);
+        mesh.alloc(&self.render_api).draw_with_texture(atlas.texture)
+    }
+
+    fn get(&mut self, i: usize) -> GfxDrawMesh {
+        assert!(i < emoji::EMOJI_LIST.len());
+
+        if i >= self.meshes.len() {
+            //d!("EmojiMeshes loading new glyphs");
+            for j in self.meshes.len()..=i {
+                let emoji = emoji::EMOJI_LIST[j];
+                let mesh = self.gen_emoji_mesh(emoji);
+                self.meshes.push(mesh);
+            }
+        }
+
+        self.meshes[i].clone()
+    }
+}
+
+struct TouchInfo {
+    start_pos: Point,
+    start_scroll: f32,
+    is_scroll: bool,
+}
+
+pub type EmojiPickerPtr = Arc<EmojiPicker>;
+
+pub struct EmojiPicker {
+    node: SceneNodeWeak,
+    render_api: RenderApi,
+    tasks: OnceLock<Vec<smol::Task<()>>>,
+
+    dc_key: u64,
+    emoji_meshes: EmojiMeshesPtr,
+
+    rect: PropertyRect,
+    z_index: PropertyUint32,
+    scroll: PropertyFloat32,
+    emoji_size: PropertyFloat32,
+    mouse_scroll_speed: PropertyFloat32,
+
+    window_scale: PropertyFloat32,
+    parent_rect: SyncMutex<Option<Rectangle>>,
+    is_mouse_hover: AtomicBool,
+    touch_info: SyncMutex<Option<TouchInfo>>,
+}
+
+impl EmojiPicker {
+    pub async fn new(
+        node: SceneNodeWeak,
+        window_scale: PropertyFloat32,
+        render_api: RenderApi,
+        emoji_meshes: EmojiMeshesPtr,
+        ex: ExecutorPtr,
+    ) -> Pimpl {
+        d!("EmojiPicker::new()");
+
+        let node_ref = &node.upgrade().unwrap();
+        let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
+        let z_index = PropertyUint32::wrap(node_ref, Role::Internal, "z_index", 0).unwrap();
+        let scroll = PropertyFloat32::wrap(node_ref, Role::Internal, "scroll", 0).unwrap();
+        let emoji_size = PropertyFloat32::wrap(node_ref, Role::Internal, "emoji_size", 0).unwrap();
+        let mouse_scroll_speed =
+            PropertyFloat32::wrap(node_ref, Role::Internal, "mouse_scroll_speed", 0).unwrap();
+
+        let node_name = node_ref.name.clone();
+        let node_id = node_ref.id;
+
+        let self_ = Arc::new(Self {
+            node,
+            render_api,
+            tasks: OnceLock::new(),
+
+            dc_key: OsRng.gen(),
+            emoji_meshes,
+
+            rect,
+            z_index,
+            scroll,
+            emoji_size,
+            mouse_scroll_speed,
+
+            window_scale,
+            parent_rect: SyncMutex::new(None),
+            is_mouse_hover: AtomicBool::new(false),
+            touch_info: SyncMutex::new(None),
+        });
+
+        Pimpl::EmojiPicker(self_)
+    }
+
+    fn emojis_per_line(&self) -> f32 {
+        let emoji_size = self.emoji_size.get();
+        let rect_w = self.rect.get().w;
+        (rect_w / emoji_size).floor()
+    }
+    fn calc_off_x(&self) -> f32 {
+        let emoji_size = self.emoji_size.get();
+        let rect_w = self.rect.get().w;
+        let n = self.emojis_per_line();
+        let off_x = (rect_w - emoji_size) / (n - 1.);
+        off_x
+    }
+
+    fn max_scroll(&self) -> f32 {
+        let emojis_len = emoji::EMOJI_LIST.len() as f32;
+        let emoji_size = self.emoji_size.get();
+        let cols = self.emojis_per_line();
+        let rows = (emojis_len / cols).floor();
+
+        let rect_h = self.rect.get().h;
+        let height = rows * emoji_size;
+        if height < rect_h {
+            return 0.
+        }
+        height - rect_h
+    }
+
+    async fn click_emoji(&self, pos: Point) {
+        //d!("click_emoji({pos:?})");
+        let emoji_size = self.emoji_size.get();
+        let col = (pos.x / emoji_size).floor();
+
+        let y = pos.y + self.scroll.get();
+        let row = (y / emoji_size).floor();
+
+        let n_cols = self.emojis_per_line();
+        //d!("col + row * n_cols = {col} + {row} * {n_cols}");
+        let idx = (col + row * n_cols).round() as usize;
+        //d!("idx = {idx}, emoji_len = {}", emoji::EMOJI_LIST.len());
+
+        if idx < emoji::EMOJI_LIST.len() {
+            let emoji = emoji::EMOJI_LIST[idx];
+            d!("Selected emoji: {emoji}");
+            let mut param_data = vec![];
+            emoji.encode(&mut param_data).unwrap();
+            let node = self.node.upgrade().unwrap();
+            node.trigger("emoji_select", param_data).await.unwrap();
+        } else {
+            d!("Index out of bounds");
+        }
+    }
+
+    fn redraw(&self) {
+        let Some(parent_rect) = self.parent_rect.lock().unwrap().clone() else { return };
+
+        let Some(draw_update) = self.get_draw_calls(parent_rect) else {
+            error!(target: "ui::emoji_picker", "Emoji picker failed to draw");
+            return;
+        };
+        self.render_api.replace_draw_calls(draw_update.draw_calls);
+        d!("replace draw calls done");
+    }
+
+    fn get_draw_calls(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
+        if let Err(e) = self.rect.eval(&parent_rect) {
+            warn!(target: "ui::emoji_picker", "Rect eval failed: {e}");
+            return None
+        }
+
+        // Clamp scroll if needed due to window size change
+        let max_scroll = self.max_scroll();
+        if self.scroll.get() > max_scroll {
+            self.scroll.set(max_scroll);
+        }
+
+        let rect = self.rect.get();
+        let mut instrs = vec![GfxDrawInstruction::ApplyView(rect)];
+
+        let off_x = self.calc_off_x();
+        let emoji_size = self.emoji_size.get();
+
+        let mut emoji_meshes = self.emoji_meshes.lock().unwrap();
+
+        let mut x = emoji_size / 2.;
+        let mut y = emoji_size / 2. - self.scroll.get();
+        for (i, mesh) in emoji::EMOJI_LIST.iter().enumerate() {
+            let pos = Point::new(x, y);
+            let mesh = emoji_meshes.get(i);
+            instrs.extend_from_slice(&[
+                GfxDrawInstruction::Move(pos),
+                GfxDrawInstruction::Draw(mesh),
+            ]);
+
+            x += off_x;
+            if x > rect.w {
+                x = emoji_size / 2.;
+                y += emoji_size;
+                //d!("Line break after idx={i}");
+            }
+
+            if y > rect.h + emoji_size {
+                break
+            }
+        }
+
+        Some(DrawUpdate {
+            key: self.dc_key,
+            draw_calls: vec![(
+                self.dc_key,
+                GfxDrawCall { instrs, dcs: vec![], z_index: self.z_index.get() },
+            )],
+        })
+    }
+}
+
+#[async_trait]
+impl UIObject for EmojiPicker {
+    fn z_index(&self) -> u32 {
+        self.z_index.get()
+    }
+
+    async fn start(self: Arc<Self>, ex: ExecutorPtr) {
+        let me = Arc::downgrade(&self);
+
+        let node_ref = &self.node.upgrade().unwrap();
+        let node_name = node_ref.name.clone();
+        let node_id = node_ref.id;
+
+        async fn redraw(self_: Arc<EmojiPicker>) {
+            self_.redraw();
+        }
+
+        let mut on_modify = OnModify::new(ex, node_name, node_id, me.clone());
+        on_modify.when_change(self.rect.prop(), redraw);
+        on_modify.when_change(self.z_index.prop(), redraw);
+
+        self.tasks.set(on_modify.tasks);
+    }
+
+    async fn draw(&self, parent_rect: Rectangle) -> Option<DrawUpdate> {
+        d!("EmojiPicker::draw()");
+        *self.parent_rect.lock().unwrap() = Some(parent_rect);
+        self.get_draw_calls(parent_rect)
+    }
+
+    async fn handle_mouse_move(&self, mut mouse_pos: Point) -> bool {
+        let rect = self.rect.get();
+        self.is_mouse_hover.store(rect.contains(mouse_pos), Ordering::Relaxed);
+        false
+    }
+
+    async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
+        if !self.is_mouse_hover.load(Ordering::Relaxed) {
+            return false
+        }
+        d!("handle_mouse_wheel()");
+
+        let mut scroll = self.scroll.get();
+        scroll -= self.mouse_scroll_speed.get() * wheel_pos.y;
+        scroll = scroll.clamp(0., self.max_scroll());
+        self.scroll.set(scroll);
+
+        self.redraw();
+
+        true
+    }
+
+    async fn handle_mouse_btn_up(&self, btn: MouseButton, mut mouse_pos: Point) -> bool {
+        let rect = self.rect.get();
+        if !rect.contains(mouse_pos) {
+            return false
+        }
+        mouse_pos.x -= rect.x;
+        mouse_pos.y -= rect.y;
+        self.click_emoji(mouse_pos).await;
+
+        true
+    }
+
+    async fn handle_touch(&self, phase: TouchPhase, id: u64, mut touch_pos: Point) -> bool {
+        // Ignore multi-touch
+        if id != 0 {
+            return false
+        }
+
+        let rect = self.rect.get();
+        let pos = touch_pos - Point::new(rect.x, rect.y);
+
+        // We need this cos you cannot hold mutex and call async fn
+        // todo: clean this up
+        let mut emoji_is_clicked = false;
+        {
+            let mut touch_info = self.touch_info.lock().unwrap();
+            match phase {
+                TouchPhase::Started => {
+                    if !rect.contains(touch_pos) {
+                        return false
+                    }
+
+                    *touch_info = Some(TouchInfo {
+                        start_pos: pos,
+                        start_scroll: self.scroll.get(),
+                        is_scroll: false,
+                    });
+                }
+                TouchPhase::Moved => {
+                    if let Some(touch_info) = touch_info.as_mut() {
+                        let y_diff = touch_info.start_pos.y - pos.y;
+                        if y_diff.abs() > 0.5 {
+                            touch_info.is_scroll = true;
+                        }
+
+                        if touch_info.is_scroll {
+                            let mut scroll = touch_info.start_scroll + y_diff;
+                            scroll = scroll.clamp(0., self.max_scroll());
+                            self.scroll.set(scroll);
+                            self.redraw();
+                        }
+                    } else {
+                        return false
+                    }
+                }
+                TouchPhase::Ended | TouchPhase::Cancelled => {
+                    if let Some(touch_info) = &*touch_info {
+                        if !touch_info.is_scroll {
+                            emoji_is_clicked = true;
+                        }
+                    } else {
+                        return false
+                    }
+                    *touch_info = None;
+                }
+            }
+        }
+        if emoji_is_clicked {
+            self.click_emoji(pos).await;
+        }
+
+        true
+    }
+}
+
+impl Drop for EmojiPicker {
+    fn drop(&mut self) {
+        self.render_api.replace_draw_calls(vec![(self.dc_key, Default::default())]);
+    }
+}

+ 1 - 1
bin/darkwallet/src/ui/mod.rs

@@ -38,7 +38,7 @@ mod chatedit;
 pub use chatedit::{ChatEdit, ChatEditPtr};
 mod editbox;
 pub use editbox::{EditBox, EditBoxPtr};
-mod emoji_picker;
+pub mod emoji_picker;
 pub use emoji_picker::{EmojiPicker, EmojiPickerPtr};
 mod image;
 pub use image::{Image, ImagePtr};