/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2025 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 .
*/
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,
},
time::Instant,
};
use crate::{
error::Result,
gfx::{
GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, GfxTextureId, GraphicsEventPublisherPtr,
Point, Rectangle, RenderApi, Vertex,
},
mesh::{Color, MeshBuilder, MeshInfo, COLOR_BLUE, COLOR_RED, COLOR_WHITE},
prop::{
PropertyAtomicGuard, PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr,
PropertyRect, PropertyStr, PropertyUint32, Role,
},
pubsub::Subscription,
scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
text::{self, Glyph, GlyphPositionIter, TextShaperPtr},
text2::{self, Editor},
util::{enumerate_ref, is_whitespace, min_f32, unixtime, zip4},
AndroidSuggestEvent, ExecutorPtr,
};
use super::{
editbox::{
editable::{Editable, RenderedEditable, Selection, TextIdx, TextPos},
eol_nudge,
repeat::{PressedKey, PressedKeysSmoothRepeat},
ALLOWED_KEYCODES, DISALLOWED_CHARS,
},
DrawUpdate, OnModify, UIObject,
};
// Minimum dist to update scroll when finger scrolling.
// Avoid updating too much makes scrolling smoother.
const VERT_SCROLL_UPDATE_INC: f32 = 1.;
macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::chatedit", $($arg)*); } }
macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::chatedit", $($arg)*); } }
// You must be careful working with string indexes in Java. They are UTF16 string indexs, not UTF8
fn char16_to_byte_index(s: &str, char_idx: usize) -> Option {
let utf16_data: Vec<_> = s.encode_utf16().take(char_idx).collect();
let prestr = String::from_utf16(&utf16_data).ok()?;
Some(prestr.len())
}
fn byte_to_char16_index(s: &str, byte_idx: usize) -> Option {
if byte_idx > s.len() || !s.is_char_boundary(byte_idx) {
return None
}
Some(s[..byte_idx].encode_utf16().count())
}
#[derive(Clone)]
struct TouchStartInfo {
pos: Point,
instant: std::time::Instant,
}
impl TouchStartInfo {
fn new(pos: Point) -> Self {
Self { pos, instant: std::time::Instant::now() }
}
}
#[derive(Clone)]
enum TouchStateAction {
Inactive,
Started { pos: Point, instant: std::time::Instant },
StartSelect,
Select,
DragSelectHandle { side: isize },
ScrollVert { start_pos: Point, scroll_start: f32 },
SetCursorPos,
}
struct TouchInfo {
state: TouchStateAction,
start: Option,
scroll: PropertyFloat32,
}
impl TouchInfo {
fn new(scroll: PropertyFloat32) -> Self {
Self { state: TouchStateAction::Inactive, start: None, scroll }
}
fn start(&mut self, pos: Point) {
debug!(target: "ui::chatedit::touch", "start touch: Started state");
self.state = TouchStateAction::Started { pos, instant: std::time::Instant::now() };
}
fn stop(&mut self) -> TouchStateAction {
debug!(target: "ui::chatedit::touch", "stop touch: Inactive state");
std::mem::replace(&mut self.state, TouchStateAction::Inactive)
}
fn update(&mut self, pos: &Point) {
match &self.state {
TouchStateAction::Started { pos: start_pos, instant } => {
let travel_dist = pos.dist_sq(*start_pos);
let grad = (pos.y - start_pos.y) / (pos.x - start_pos.x);
let elapsed = instant.elapsed().as_millis();
//debug!(target: "ui::chatedit::touch", "TouchInfo::update() [travel_dist={travel_dist}, grad={grad}]");
if travel_dist < 5. {
if elapsed > 1000 {
debug!(target: "ui::chatedit::touch", "update touch state: Started -> StartSelect");
self.state = TouchStateAction::StartSelect;
}
} else if grad.abs() > 0.5 {
// Vertical movement
debug!(target: "ui::chatedit::touch", "update touch state: Started -> ScrollVert");
let scroll_start = self.scroll.get();
self.state =
TouchStateAction::ScrollVert { start_pos: *start_pos, scroll_start };
} else {
// Horizontal movement
debug!(target: "ui::chatedit::touch", "update touch state: Started -> SetCursorPos");
self.state = TouchStateAction::SetCursorPos;
}
}
_ => {}
}
}
}
/*
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, "'")
}
}
*/
enum ColoringState {
Start,
IsCommand,
Normal,
}
pub type ChatEditPtr = Arc;
pub struct ChatEdit {
node: SceneNodeWeak,
tasks: OnceLock>>,
render_api: RenderApi,
text_shaper: TextShaperPtr,
key_repeat: SyncMutex,
// Moves the draw cursor and applies scroll
root_dc_key: u64,
phone_select_handle_dc_key: u64,
// Applies the clipping view
content_dc_key: u64,
select_dc_key: u64,
text_dc_key: u64,
cursor_dc_key: u64,
cursor_mesh: SyncMutex