mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
394 lines
12 KiB
Rust
394 lines
12 KiB
Rust
/* 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 async_trait::async_trait;
|
|
use darkfi_serial::Encodable;
|
|
use miniquad::{MouseButton, TouchPhase};
|
|
use parking_lot::Mutex as SyncMutex;
|
|
use rand::{rngs::OsRng, Rng};
|
|
use std::sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc,
|
|
};
|
|
|
|
use crate::{
|
|
gfx::{gfxtag, DrawCall, DrawInstruction, Point, Rectangle, RenderApi},
|
|
prop::{
|
|
BatchGuardPtr, PropertyAtomicGuard, PropertyFloat32, PropertyRect, PropertyUint32, Role,
|
|
},
|
|
scene::{Pimpl, SceneNodeWeak},
|
|
ExecutorPtr,
|
|
};
|
|
|
|
use super::{DrawUpdate, OnModify, UIObject};
|
|
|
|
mod default;
|
|
use default::DEFAULT_EMOJI_LIST;
|
|
mod emoji;
|
|
pub use emoji::{EmojiMeshes, EmojiMeshesPtr};
|
|
|
|
macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::emoji_picker", $($arg)*) } }
|
|
macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::emoji_picker", $($arg)*) } }
|
|
|
|
#[derive(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: SyncMutex<Vec<smol::Task<()>>>,
|
|
|
|
dc_key: u64,
|
|
emoji_meshes: EmojiMeshesPtr,
|
|
|
|
rect: PropertyRect,
|
|
z_index: PropertyUint32,
|
|
priority: PropertyUint32,
|
|
scroll: PropertyFloat32,
|
|
emoji_size: PropertyFloat32,
|
|
mouse_scroll_speed: PropertyFloat32,
|
|
|
|
parent_rect: SyncMutex<Option<Rectangle>>,
|
|
is_mouse_hover: AtomicBool,
|
|
touch_info: SyncMutex<Option<TouchInfo>>,
|
|
}
|
|
|
|
impl EmojiPicker {
|
|
pub async fn new(
|
|
node: SceneNodeWeak,
|
|
render_api: RenderApi,
|
|
emoji_meshes: EmojiMeshesPtr,
|
|
) -> Pimpl {
|
|
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 priority = PropertyUint32::wrap(node_ref, Role::Internal, "priority", 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 self_ = Arc::new(Self {
|
|
node,
|
|
render_api,
|
|
tasks: SyncMutex::new(vec![]),
|
|
|
|
dc_key: OsRng.gen(),
|
|
emoji_meshes,
|
|
|
|
rect,
|
|
z_index,
|
|
priority,
|
|
scroll,
|
|
emoji_size,
|
|
mouse_scroll_speed,
|
|
|
|
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;
|
|
//d!("rect_w = {rect_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 = DEFAULT_EMOJI_LIST.len() as f32;
|
|
let emoji_size = self.emoji_size.get();
|
|
let cols = self.emojis_per_line();
|
|
let rows = (emojis_len / cols).ceil();
|
|
|
|
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) {
|
|
let n_cols = self.emojis_per_line();
|
|
let emoji_size = self.emoji_size.get();
|
|
let scroll = self.scroll.get();
|
|
|
|
// Emojis have spacing along the x axis.
|
|
// If the screen width is 2000, and emoji_size is 30, then that's 66 emojis.
|
|
// But that's 66.66px per emoji.
|
|
let real_width = self.rect.get().w / n_cols;
|
|
//d!("click_emoji({pos:?})");
|
|
let col = (pos.x / real_width).floor();
|
|
|
|
let y = pos.y + scroll;
|
|
let row = (y / emoji_size).floor();
|
|
//d!("emoji_size = {emoji_size}, col = {col}, row = {row}");
|
|
|
|
//d!("idx = col + row * n_cols = {col} + {row} * {n_cols}");
|
|
let idx = (col + row * n_cols).round() as usize;
|
|
//d!(" = {idx}, emoji_len = {}", emoji::EMOJI_LIST.len());
|
|
|
|
let emoji_selected = {
|
|
if idx < DEFAULT_EMOJI_LIST.len() {
|
|
let emoji = DEFAULT_EMOJI_LIST[idx].to_string();
|
|
Some(emoji)
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
match emoji_selected {
|
|
Some(emoji) => {
|
|
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();
|
|
}
|
|
None => d!("Index out of bounds: {idx}"),
|
|
}
|
|
}
|
|
|
|
#[instrument(target = "ui::emoji_picker")]
|
|
async fn redraw(&self, atom: &mut PropertyAtomicGuard) {
|
|
let Some(parent_rect) = self.parent_rect.lock().clone() else { return };
|
|
|
|
let Some(draw_update) = self.get_draw_calls(parent_rect, atom).await else {
|
|
error!(target: "ui:emoji_picker", "Emoji picker failed to draw");
|
|
return
|
|
};
|
|
self.render_api.replace_draw_calls(atom.batch_id, draw_update.draw_calls);
|
|
}
|
|
|
|
async fn get_draw_calls(
|
|
&self,
|
|
parent_rect: Rectangle,
|
|
atom: &mut PropertyAtomicGuard,
|
|
) -> Option<DrawUpdate> {
|
|
if let Err(e) = self.rect.eval(atom, &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(atom, max_scroll);
|
|
}
|
|
|
|
let rect = self.rect.get();
|
|
let mut instrs = vec![DrawInstruction::ApplyView(rect)];
|
|
|
|
let off_x = self.calc_off_x();
|
|
let emoji_size = self.emoji_size.get();
|
|
|
|
let mut x = 0.;
|
|
let mut y = -self.scroll.get();
|
|
for i in 0..DEFAULT_EMOJI_LIST.len() {
|
|
let pos = Point::new(x, y);
|
|
let mesh = self.emoji_meshes.lock().get(i);
|
|
instrs.extend_from_slice(&[DrawInstruction::SetPos(pos), DrawInstruction::Draw(mesh)]);
|
|
|
|
x += off_x;
|
|
if x > rect.w {
|
|
x = 0.;
|
|
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,
|
|
DrawCall::new(instrs, vec![], self.z_index.get(), "emoji"),
|
|
)],
|
|
})
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl UIObject for EmojiPicker {
|
|
fn priority(&self) -> u32 {
|
|
self.priority.get()
|
|
}
|
|
|
|
async fn start(self: Arc<Self>, ex: ExecutorPtr) {
|
|
let me = Arc::downgrade(&self);
|
|
|
|
async fn redraw(self_: Arc<EmojiPicker>, batch: BatchGuardPtr) {
|
|
let atom = &mut batch.spawn();
|
|
self_.redraw(atom).await;
|
|
}
|
|
|
|
let mut on_modify = OnModify::new(ex, self.node.clone(), me.clone());
|
|
on_modify.when_change(self.rect.prop(), redraw);
|
|
on_modify.when_change(self.z_index.prop(), redraw);
|
|
|
|
*self.tasks.lock() = on_modify.tasks;
|
|
}
|
|
|
|
fn stop(&self) {
|
|
self.tasks.lock().clear();
|
|
self.emoji_meshes.lock().clear();
|
|
}
|
|
|
|
#[instrument(target = "ui::emoji_picker")]
|
|
async fn draw(
|
|
&self,
|
|
parent_rect: Rectangle,
|
|
atom: &mut PropertyAtomicGuard,
|
|
) -> Option<DrawUpdate> {
|
|
*self.parent_rect.lock() = Some(parent_rect);
|
|
self.get_draw_calls(parent_rect, atom).await
|
|
}
|
|
|
|
async fn handle_mouse_move(&self, 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
|
|
}
|
|
t!("handle_mouse_wheel()");
|
|
let atom = &mut self.render_api.make_guard(gfxtag!("EmojiPicker::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(atom, scroll);
|
|
|
|
self.redraw(atom).await;
|
|
|
|
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, touch_pos: Point) -> bool {
|
|
// Ignore multi-touch
|
|
if id != 0 {
|
|
return false
|
|
}
|
|
|
|
let atom = &mut self.render_api.make_guard(gfxtag!("EmojiPicker::handle_touch"));
|
|
|
|
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;
|
|
{
|
|
match phase {
|
|
TouchPhase::Started => {
|
|
let mut touch_info = self.touch_info.lock();
|
|
if !rect.contains(touch_pos) {
|
|
return false
|
|
}
|
|
|
|
*touch_info = Some(TouchInfo {
|
|
start_pos: pos,
|
|
start_scroll: self.scroll.get(),
|
|
is_scroll: false,
|
|
});
|
|
}
|
|
TouchPhase::Moved => {
|
|
let (touch_info, y_diff) = {
|
|
let mut touch_info = self.touch_info.lock();
|
|
let Some(touch_info) = touch_info.as_mut() else {
|
|
return false;
|
|
};
|
|
|
|
let y_diff = touch_info.start_pos.y - pos.y;
|
|
if y_diff.abs() > 0.5 {
|
|
touch_info.is_scroll = true;
|
|
}
|
|
(touch_info.clone(), y_diff)
|
|
};
|
|
|
|
if touch_info.is_scroll {
|
|
let mut scroll = touch_info.start_scroll + y_diff;
|
|
scroll = scroll.clamp(0., self.max_scroll());
|
|
self.scroll.set(atom, scroll);
|
|
self.redraw(atom).await;
|
|
}
|
|
}
|
|
TouchPhase::Ended | TouchPhase::Cancelled => {
|
|
let touch_info = std::mem::take(&mut *self.touch_info.lock());
|
|
let Some(touch_info) = touch_info else { return false };
|
|
if !touch_info.is_scroll {
|
|
emoji_is_clicked = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if emoji_is_clicked {
|
|
self.click_emoji(pos).await;
|
|
}
|
|
|
|
true
|
|
}
|
|
}
|
|
|
|
impl Drop for EmojiPicker {
|
|
fn drop(&mut self) {
|
|
let atom = self.render_api.make_guard(gfxtag!("EmojiPicker::drop"));
|
|
self.render_api.replace_draw_calls(atom.batch_id, vec![(self.dc_key, Default::default())]);
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for EmojiPicker {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
write!(f, "{:?}", self.node.upgrade().unwrap())
|
|
}
|
|
}
|