diff --git a/bin/darkwallet/src/app/node.rs b/bin/darkwallet/src/app/node.rs index 882fd6065165..27245fcf7633 100644 --- a/bin/darkwallet/src/app/node.rs +++ b/bin/darkwallet/src/app/node.rs @@ -312,6 +312,7 @@ pub fn create_chatedit(name: &str) -> SceneNode { node.add_property(prop).unwrap(); let mut prop = Property::new("cursor_pos", PropertyType::Uint32, PropertySubType::Pixel); + prop.set_ui_text("Cursor Pos", "Cursor position within the text"); prop.set_range_u32(0, u32::MAX); node.add_property(prop).unwrap(); @@ -404,6 +405,7 @@ pub fn create_chatedit(name: &str) -> SceneNode { node.add_signal("enter_pressed", "Enter key pressed", vec![]).unwrap(); node.add_signal("keyboard_request", "Request to show keyboard", vec![]).unwrap(); + node.add_signal("paste_request", "Request to show paste dialog", vec![]).unwrap(); // Used by emoji_picker node.add_method("insert_text", vec![("text", "Text", CallArgType::Str)], None).unwrap(); diff --git a/bin/darkwallet/src/app/schema/chat.rs b/bin/darkwallet/src/app/schema/chat.rs index 81b6820c690e..81d05456dfff 100644 --- a/bin/darkwallet/src/app/schema/chat.rs +++ b/bin/darkwallet/src/app/schema/chat.rs @@ -784,6 +784,7 @@ pub async fn make( let editz_text = PropertyStr::wrap(&node, Role::App, "text", 0).unwrap(); let editz_select_text = node.get_property("select_text").unwrap(); + let editz_cursor_pos = node.get_property("cursor_pos").unwrap(); //let editbox_focus = PropertyBool::wrap(node, Role::App, "is_focused", 0).unwrap(); //let darkirc_backend = app.darkirc_backend.clone(); @@ -992,31 +993,6 @@ pub async fn make( let cmd_hint_is_visible = PropertyBool::wrap(&cmd_layer_node, Role::App, "is_visible", 0).unwrap(); - let editz_text_sub = editz_text.prop().subscribe_modify(); - let editz_text_task = app.ex.spawn(async move { - while let Ok(_) = editz_text_sub.receive().await { - let text = editz_text.get(); - info!(target: "app::chat", "text changed: {text}"); - // We want to avoid setting the property multiple times to the same value - // because then it triggers unnecessary redraw work. - if let Some(first_char) = text.chars().next() { - if first_char == '/' { - if !cmd_hint_is_visible.get() { - cmd_hint_is_visible.set(true); - } - } else { - if cmd_hint_is_visible.get() { - cmd_hint_is_visible.set(false); - } - } - } else { - if cmd_hint_is_visible.get() { - cmd_hint_is_visible.set(false); - } - } - } - }); - app.tasks.lock().unwrap().push(editz_text_task); // Create the actionbar bg let node = create_vector_art("cmd_hint_bg"); @@ -1173,7 +1149,7 @@ pub async fn make( layer_node.set_property_u32(Role::App, "priority", 1).unwrap(); let layer_node = layer_node.setup(|me| Layer::new(me, app.render_api.clone(), app.ex.clone())).await; - content_layer_node.link(layer_node.clone()); + content_layer_node.clone().link(layer_node.clone()); let actions_is_visible = PropertyBool::wrap(&layer_node, Role::App, "is_visible", 0).unwrap(); @@ -1325,6 +1301,7 @@ pub async fn make( let (slot, recvr) = Slot::new("paste_clicked"); node.register("click", slot).unwrap(); let actions_is_visible2 = actions_is_visible.clone(); + let chatedit_node2 = chatedit_node.clone(); let listen_click = app.ex.spawn(async move { let mut clip = Clipboard::new(); while let Ok(_) = recvr.recv().await { @@ -1332,7 +1309,7 @@ pub async fn make( info!(target: "app::chat", "clicked paste: {text}"); let mut data = vec![]; text.encode(&mut data).unwrap(); - chatedit_node.call_method("insert_text", data).await.unwrap(); + chatedit_node2.call_method("insert_text", data).await.unwrap(); } else { info!(target: "app::chat", "clicked paste but clip is empty"); } @@ -1366,18 +1343,192 @@ pub async fn make( let node = node.setup(|me| Button::new(me, app.ex.clone())).await; layer_node.clone().link(node); + // Paste overlay popup + let layer_node = create_layer("paste_layer"); + let prop = layer_node.get_property("rect").unwrap(); + prop.set_f32(Role::App, 0, 40.).unwrap(); + let code = cc.compile("editz_bg_top_y - ACTION_POPUP_Y_OFF").unwrap(); + //let code = cc.compile("h - 60 - 80").unwrap(); + //let code = cc.compile("h - 60 - 300").unwrap(); + prop.set_expr(Role::App, 1, code).unwrap(); + prop.set_f32(Role::App, 2, ACTION_SELECT_ALL_RECT.rhs()).unwrap(); + prop.set_f32(Role::App, 3, ACTION_SELECT_ALL_RECT.h).unwrap(); + prop.add_depend(&editbox_bg_rect_prop, 1, "editz_bg_top_y"); + layer_node.set_property_bool(Role::App, "is_visible", false).unwrap(); + layer_node.set_property_u32(Role::App, "z_index", 8).unwrap(); + // Priority higher than chatview but lower than chatedit + layer_node.set_property_u32(Role::App, "priority", 1).unwrap(); + let layer_node = + layer_node.setup(|me| Layer::new(me, app.render_api.clone(), app.ex.clone())).await; + content_layer_node.link(layer_node.clone()); + + let pasta_is_visible = PropertyBool::wrap(&layer_node, Role::App, "is_visible", 0).unwrap(); + + let (slot, recvr) = Slot::new("reqpasta"); + chatedit_node.register("paste_request", slot).unwrap(); + let pasta_is_visible2 = pasta_is_visible.clone(); + let listen_click = app.ex.spawn(async move { + while let Ok(_) = recvr.recv().await { + pasta_is_visible2.set(true); + } + }); + app.tasks.lock().unwrap().push(listen_click); + + // Create the paste bg + let node = create_vector_art("paste_bg"); + let prop = node.get_property("rect").unwrap(); + prop.set_f32(Role::App, 0, 0.).unwrap(); + prop.set_f32(Role::App, 1, 0.).unwrap(); + prop.set_f32(Role::App, 2, ACTION_SELECT_ALL_RECT.rhs()).unwrap(); + prop.set_f32(Role::App, 3, ACTION_SELECT_ALL_RECT.h).unwrap(); + node.set_property_u32(Role::App, "z_index", 0).unwrap(); + + let mut shape = VectorShape::new(); + + let color1 = [0., 0.13, 0.08, 0.75]; + let color2 = [0., 0., 0., 1.]; + let gradient = [color1.clone(), color1, color2.clone(), color2]; + let hicolor = [0., 0.94, 1., 1.]; + + // Paste box + shape.add_gradient_box( + expr::const_f32(0.), + expr::const_f32(0.), + expr::const_f32(ACTION_PASTE_RECT.w), + expr::const_f32(ACTION_PASTE_RECT.h), + gradient.clone(), + ); + + // Paste outline + shape.add_outline( + expr::const_f32(0.), + expr::const_f32(0.), + expr::const_f32(ACTION_PASTE_RECT.w), + expr::const_f32(ACTION_PASTE_RECT.h), + 1., + hicolor.clone(), + ); + + let node = + node.setup(|me| VectorArt::new(me, shape, app.render_api.clone(), app.ex.clone())).await; + layer_node.clone().link(node); + + // Create some text + let node = create_text("paste_label"); + let prop = node.get_property("rect").unwrap(); + prop.set_f32(Role::App, 0, ACTION_LABEL_POS.x).unwrap(); + prop.set_f32(Role::App, 1, ACTION_LABEL_POS.y).unwrap(); + prop.set_f32(Role::App, 2, ACTION_SELECT_ALL_RECT.rhs()).unwrap(); + prop.set_f32(Role::App, 3, ACTION_SELECT_ALL_RECT.h).unwrap(); + node.set_property_f32(Role::App, "baseline", 0.).unwrap(); + node.set_property_f32(Role::App, "font_size", FONTSIZE).unwrap(); + node.set_property_str(Role::App, "text", "paste").unwrap(); + //node.set_property_bool(Role::App, "debug", true).unwrap(); + //node.set_property_str(Role::App, "text", "anon1").unwrap(); + let prop = node.get_property("text_color").unwrap(); + prop.set_f32(Role::App, 0, 0.).unwrap(); + prop.set_f32(Role::App, 1, 0.94).unwrap(); + prop.set_f32(Role::App, 2, 1.).unwrap(); + prop.set_f32(Role::App, 3, 1.).unwrap(); + node.set_property_u32(Role::App, "z_index", 1).unwrap(); + + let node = node + .setup(|me| { + Text::new( + me, + window_scale.clone(), + app.render_api.clone(), + app.text_shaper.clone(), + app.ex.clone(), + ) + }) + .await; + layer_node.clone().link(node); + + // Paste button + let node = create_button("paste_btn"); + node.set_property_bool(Role::App, "is_active", true).unwrap(); + let prop = node.get_property("rect").unwrap(); + prop.set_f32(Role::App, 0, 0.).unwrap(); + prop.set_f32(Role::App, 1, 0.).unwrap(); + prop.set_f32(Role::App, 2, ACTION_PASTE_RECT.w).unwrap(); + prop.set_f32(Role::App, 3, ACTION_PASTE_RECT.h).unwrap(); + + let node = node.setup(|me| Button::new(me, app.ex.clone())).await; + layer_node.clone().link(node.clone()); + + let (slot, recvr) = Slot::new("paste_clicked"); + node.register("click", slot).unwrap(); + let chatedit_node2 = chatedit_node.clone(); + let pasta_is_visible2 = pasta_is_visible.clone(); + let listen_click = app.ex.spawn(async move { + let mut clip = Clipboard::new(); + while let Ok(_) = recvr.recv().await { + if let Some(text) = clip.get() { + info!(target: "app::chat", "clicked paste: {text}"); + let mut data = vec![]; + text.encode(&mut data).unwrap(); + chatedit_node2.call_method("insert_text", data).await.unwrap(); + } else { + info!(target: "app::chat", "clicked paste but clip is empty"); + } + pasta_is_visible2.set(false); + } + }); + app.tasks.lock().unwrap().push(listen_click); + + let editz_cpos_sub = editz_cursor_pos.subscribe_modify(); + let pasta_is_visible2 = pasta_is_visible.clone(); + let editz_cpos_task = app.ex.spawn(async move { + while let Ok(_) = editz_cpos_sub.receive().await { + pasta_is_visible2.set(false); + } + }); + app.tasks.lock().unwrap().push(editz_cpos_task); + let editz_select_sub = editz_select_text.subscribe_modify(); + let pasta_is_visible2 = pasta_is_visible.clone(); let editz_select_task = app.ex.spawn(async move { while let Ok(_) = editz_select_sub.receive().await { if editz_select_text.is_null(0).unwrap() { info!(target: "app::chat", "selection changed: null"); actions_is_visible.set(false); + pasta_is_visible2.set(false); } else { let select_text = editz_select_text.get_str(0).unwrap(); info!(target: "app::chat", "selection changed: {select_text}"); actions_is_visible.set(true); + pasta_is_visible2.set(false); } } }); app.tasks.lock().unwrap().push(editz_select_task); + + let editz_text_sub = editz_text.prop().subscribe_modify(); + let editz_text_task = app.ex.spawn(async move { + while let Ok(_) = editz_text_sub.receive().await { + pasta_is_visible.set(false); + + let text = editz_text.get(); + info!(target: "app::chat", "text changed: {text}"); + // We want to avoid setting the property multiple times to the same value + // because then it triggers unnecessary redraw work. + if let Some(first_char) = text.chars().next() { + if first_char == '/' { + if !cmd_hint_is_visible.get() { + cmd_hint_is_visible.set(true); + } + } else { + if cmd_hint_is_visible.get() { + cmd_hint_is_visible.set(false); + } + } + } else { + if cmd_hint_is_visible.get() { + cmd_hint_is_visible.set(false); + } + } + } + }); + app.tasks.lock().unwrap().push(editz_text_task); } diff --git a/bin/darkwallet/src/ui/chatedit.rs b/bin/darkwallet/src/ui/chatedit.rs index b333b0dcfd2d..640c1dab9552 100644 --- a/bin/darkwallet/src/ui/chatedit.rs +++ b/bin/darkwallet/src/ui/chatedit.rs @@ -170,6 +170,7 @@ impl TextWrap { fn get_word_boundary(&mut self, pos: TextPos) -> (TextPos, TextPos) { let rendered = self.get_render(); + let final_pos = rendered.glyphs.len(); // Find word start let mut pos_start = pos; @@ -183,8 +184,8 @@ impl TextWrap { } // Find word end - let mut pos_end = pos + 1; - while pos_end < rendered.glyphs.len() { + let mut pos_end = std::cmp::min(pos + 1, final_pos); + while pos_end < final_pos { let glyph_str = &rendered.glyphs[pos_end].substr; if is_whitespace(glyph_str) { break @@ -646,10 +647,10 @@ impl ChatEdit { // .lock() // .editable // .set_text("".to_string(), "king!๐Ÿ˜๐Ÿ†jelly ๐Ÿ†1234".to_string()); - self_.text_wrap.lock().editable.set_text( - "".to_string(), - "A berry is a small, pulpy, and often edible fruit. Typically, berries are juicy, rounded, brightly colored, sweet, sour or tart, and do not have a stone or pit, although many pips or seeds may be present. Common examples of berries in the culinary sense are strawberries, raspberries, blueberries, blackberries, white currants, blackcurrants, and redcurrants. In Britain, soft fruit is a horticultural term for such fruits. The common usage of the term berry is different from the scientific or botanical definition of a berry, which refers to a fruit produced from the ovary of a single flower where the outer layer of the ovary wall develops into an edible fleshy portion (pericarp). The botanical definition includes many fruits that are not commonly known or referred to as berries, such as grapes, tomatoes, cucumbers, eggplants, bananas, and chili peppers.".to_string() - ); + //self_.text_wrap.lock().editable.set_text( + // "".to_string(), + // "A berry is a small, pulpy, and often edible fruit. Typically, berries are juicy, rounded, brightly colored, sweet, sour or tart, and do not have a stone or pit, although many pips or seeds may be present. Common examples of berries in the culinary sense are strawberries, raspberries, blueberries, blackberries, white currants, blackcurrants, and redcurrants. In Britain, soft fruit is a horticultural term for such fruits. The common usage of the term berry is different from the scientific or botanical definition of a berry, which refers to a fruit produced from the ovary of a single flower where the outer layer of the ovary wall develops into an edible fleshy portion (pericarp). The botanical definition includes many fruits that are not commonly known or referred to as berries, such as grapes, tomatoes, cucumbers, eggplants, bananas, and chili peppers.".to_string() + //); //self_ // .text_wrap // .lock() @@ -689,7 +690,6 @@ impl ChatEdit { let linespacing = self.linespacing.get(); let baseline = self.baseline.get(); let scroll = self.scroll.get(); - let cursor_pos = self.cursor_pos.get() as usize; let cursor_color = self.cursor_color.get(); let debug = self.debug.get(); @@ -1275,23 +1275,23 @@ impl ChatEdit { // begin selection let select = &mut text_wrap.select; select.clear(); - select.push(Selection::new(word_start, word_end)); + if word_start != word_end { + select.push(Selection::new(word_start, word_end)); + + self.is_phone_select.store(true, Ordering::Relaxed); + // redraw() will now hide the cursor + self.hide_cursor.store(true, Ordering::Relaxed); + } debug!(target: "ui::chatview", "Selected {select:?} from {touch_pos:?}"); self.update_select_text(&mut text_wrap); - - self.is_phone_select.store(true, Ordering::Relaxed); - // redraw() will now hide the cursor - self.hide_cursor.store(true, Ordering::Relaxed); } - // Call this whenever the selection changes + /// Call this whenever the selection changes to update the external property fn update_select_text(&self, text_wrap: &mut TextWrap) { let select = &text_wrap.select; let Some(select) = select.first().cloned() else { - if !self.select_text.is_null(0).unwrap() { - self.select_text.set_null(Role::Internal, 0).unwrap(); - } + self.select_text.set_null(Role::Internal, 0).unwrap(); return }; @@ -1304,6 +1304,12 @@ impl ChatEdit { self.select_text.set_str(Role::Internal, 0, text).unwrap(); } + /// Call this whenever the cursor pos changes to update the external property + fn update_cursor_pos(&self, text_wrap: &mut TextWrap) { + let cursor_off = text_wrap.editable.get_text_before().len() as u32; + self.cursor_pos.set(cursor_off); + } + /* fn copy_highlighted(&self) -> Result<()> { let start = self.selected.get_u32(0)? as usize; @@ -1439,9 +1445,14 @@ impl ChatEdit { match &touch_state { TouchStateAction::Inactive => return false, TouchStateAction::StartSelect => { - self.abs_to_local(&mut touch_pos); - self.start_touch_select(touch_pos); - self.redraw().await; + if self.text.get().is_empty() { + let node = self.node.upgrade().unwrap(); + node.trigger("paste_request", vec![]).await.unwrap(); + } else { + self.abs_to_local(&mut touch_pos); + self.start_touch_select(touch_pos); + self.redraw().await; + } debug!(target: "ui::chatedit::touch", "touch state: StartSelect -> Select"); self.touch_info.lock().state = TouchStateAction::Select; } @@ -1541,10 +1552,14 @@ impl ChatEdit { { let mut text_wrap = self.text_wrap.lock(); let cursor_pos = text_wrap.set_cursor_with_point(touch_pos, width); + self.update_cursor_pos(&mut text_wrap); let select = &mut text_wrap.select; + let select_is_empty = select.is_empty(); select.clear(); - self.update_select_text(&mut text_wrap); + if !select_is_empty { + self.update_select_text(&mut text_wrap); + } } self.is_phone_select.store(false, Ordering::Relaxed); @@ -1966,12 +1981,16 @@ impl UIObject for ChatEdit { { let mut text_wrap = self.text_wrap.lock(); let cursor_pos = text_wrap.set_cursor_with_point(mouse_pos, width); + self.update_cursor_pos(&mut text_wrap); debug!(target: "ui::editbox", "Mouse move cursor pos to {cursor_pos}"); // begin selection let select = &mut text_wrap.select; + let select_is_empty = select.is_empty(); select.clear(); - self.update_select_text(&mut text_wrap); + if !select_is_empty { + self.update_select_text(&mut text_wrap); + } self.mouse_btn_held.store(true, Ordering::Relaxed); } @@ -2016,6 +2035,7 @@ impl UIObject for ChatEdit { { let mut text_wrap = self.text_wrap.lock(); let cursor_pos = text_wrap.set_cursor_with_point(mouse_pos, width); + self.update_cursor_pos(&mut text_wrap); // modify current selection let select = &mut text_wrap.select; diff --git a/bin/darkwallet/src/ui/editbox/editable.rs b/bin/darkwallet/src/ui/editbox/editable.rs index 1c033a8bfd40..e10633961178 100644 --- a/bin/darkwallet/src/ui/editbox/editable.rs +++ b/bin/darkwallet/src/ui/editbox/editable.rs @@ -239,7 +239,7 @@ impl Editable { self.before_text += &final_text; } - fn get_text_before(&self) -> String { + pub fn get_text_before(&self) -> String { let text = self.before_text.clone() + &self.composer.commit_text + &self.composer.compose_text; text