/* 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 .
*/
use async_lock::Mutex as AsyncMutex;
use async_trait::async_trait;
use atomic_float::AtomicF32;
use chrono::{Local, TimeZone};
use darkfi::system::{msleep, CondVar};
use darkfi_serial::{deserialize, Decodable, Encodable, SerialDecodable, SerialEncodable};
use miniquad::{KeyCode, KeyMods, MouseButton, TouchPhase};
use rand::{rngs::OsRng, Rng};
use sled_overlay::sled;
use std::{
collections::VecDeque,
hash::{DefaultHasher, Hash, Hasher},
io::Cursor,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex as SyncMutex, OnceLock, Weak,
},
};
mod page;
use page::MessageBuffer;
use crate::{
gfx::{
GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, GraphicsEventPublisherPtr, Point, Rectangle,
RenderApi,
},
mesh::{Color, MeshBuilder, COLOR_BLUE, COLOR_GREEN},
prop::{
PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr, PropertyRect, PropertyUint32,
Role,
},
pubsub::Subscription,
scene::{MethodCallSub, Pimpl, SceneNodeWeak},
text::{self, Glyph, GlyphPositionIter, TextShaperPtr},
util::{enumerate, is_whitespace, unixtime},
ExecutorPtr,
};
use super::{DrawUpdate, OnModify, UIObject};
const EPSILON: f32 = 0.001;
const BIG_EPSILON: f32 = 0.05;
// Disable selecting lines for this release.
const ENABLE_SELECT: bool = false;
fn is_zero(x: f32) -> bool {
x.abs() < EPSILON
}
/// std::cmp::max() doesn't work on f32
fn max(a: f32, b: f32) -> f32 {
if a > b {
a
} else {
b
}
}
#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
pub struct ChatMsg {
pub nick: String,
pub text: String,
}
pub type Timestamp = u64;
#[derive(Clone, SerialEncodable, SerialDecodable, PartialEq)]
pub struct MessageId(pub [u8; 32]);
impl std::fmt::Display for MessageId {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
for b in &self.0 {
write!(f, "{b:02x}")?
}
Ok(())
}
}
const PRELOAD_PAGES: usize = 1;
#[derive(Clone)]
struct TouchInfo {
start_scroll: f32,
start_y: f32,
start_instant: std::time::Instant,
/// Used for flick scrolling
samples: VecDeque<(std::time::Instant, f32)>,
last_instant: std::time::Instant,
last_y: f32,
/// Selection started?
is_select_mode: Option,
}
impl TouchInfo {
fn new(start_scroll: f32, y: f32) -> Self {
Self {
start_scroll,
start_y: y,
start_instant: std::time::Instant::now(),
samples: VecDeque::from([(std::time::Instant::now(), y)]),
last_instant: std::time::Instant::now(),
last_y: y,
is_select_mode: None,
}
}
fn push_sample(&mut self, y: f32) {
self.samples.push_back((std::time::Instant::now(), y));
// Now drop all old samples older than 40ms
while let Some((instant, _)) = self.samples.front() {
if instant.elapsed().as_millis_f32() <= 40. {
break
}
self.samples.pop_front().unwrap();
}
}
fn first_sample(&self) -> Option<(f32, f32)> {
self.samples.front().map(|(t, s)| (t.elapsed().as_millis_f32(), *s))
}
}
pub type ChatViewPtr = Arc;
pub struct ChatView {
node: SceneNodeWeak,
tasks: OnceLock>>,
render_api: RenderApi,
text_shaper: TextShaperPtr,
tree: sled::Tree,
msgbuf: AsyncMutex,
dc_key: u64,
/// Used for detecting when scrolling view
mouse_pos: SyncMutex,
/// Touch scrolling
touch_info: SyncMutex>,
touch_is_active: AtomicBool,
rect: PropertyRect,
scroll: PropertyFloat32,
z_index: PropertyUint32,
priority: PropertyUint32,
scroll_start_accel: PropertyFloat32,
scroll_resist: PropertyFloat32,
select_hold_time: PropertyFloat32,
key_scroll_speed: PropertyFloat32,
/// Scroll accel
motion_cv: Arc,
speed: AtomicF32,
mouse_btn_held: AtomicBool,
/// Triggers the background loading task to wake up
bgload_cv: Arc,
/// We use it when we re-eval rect when its changed via property.
parent_rect: SyncMutex>,
}
impl ChatView {
pub async fn new(
node: SceneNodeWeak,
tree: sled::Tree,
window_scale: PropertyFloat32,
render_api: RenderApi,
text_shaper: TextShaperPtr,
ex: ExecutorPtr,
) -> Pimpl {
debug!(target: "ui::chatview", "ChatView::new()");
let node_ref = &node.upgrade().unwrap();
let rect = PropertyRect::wrap(node_ref, Role::Internal, "rect").unwrap();
let scroll = PropertyFloat32::wrap(node_ref, Role::Internal, "scroll", 0).unwrap();
let font_size = PropertyFloat32::wrap(node_ref, Role::Internal, "font_size", 0).unwrap();
let timestamp_font_size =
PropertyFloat32::wrap(node_ref, Role::Internal, "timestamp_font_size", 0).unwrap();
let timestamp_width =
PropertyFloat32::wrap(node_ref, Role::Internal, "timestamp_width", 0).unwrap();
let line_height =
PropertyFloat32::wrap(node_ref, Role::Internal, "line_height", 0).unwrap();
let message_spacing =
PropertyFloat32::wrap(node_ref, Role::Internal, "message_spacing", 0).unwrap();
let baseline = PropertyFloat32::wrap(node_ref, Role::Internal, "baseline", 0).unwrap();
let timestamp_color =
PropertyColor::wrap(node_ref, Role::Internal, "timestamp_color").unwrap();
let text_color = PropertyColor::wrap(node_ref, Role::Internal, "text_color").unwrap();
let nick_colors = node_ref.get_property("nick_colors").expect("ChatView::nick_colors");
let hi_bg_color = PropertyColor::wrap(node_ref, Role::Internal, "hi_bg_color").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 debug = PropertyBool::wrap(node_ref, Role::Internal, "debug", 0).unwrap();
let scroll_start_accel =
PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_start_accel", 0).unwrap();
let scroll_resist =
PropertyFloat32::wrap(node_ref, Role::Internal, "scroll_resist", 0).unwrap();
let select_hold_time =
PropertyFloat32::wrap(node_ref, Role::Internal, "select_hold_time", 0).unwrap();
let key_scroll_speed =
PropertyFloat32::wrap(node_ref, Role::Internal, "key_scroll_speed", 0).unwrap();
let motion_cv = Arc::new(CondVar::new());
let bgload_cv = Arc::new(CondVar::new());
let self_ = Arc::new(Self {
node: node.clone(),
tasks: OnceLock::new(),
render_api: render_api.clone(),
text_shaper: text_shaper.clone(),
tree,
msgbuf: AsyncMutex::new(MessageBuffer::new(
node,
font_size,
timestamp_font_size,
timestamp_width,
line_height,
message_spacing,
baseline,
timestamp_color,
text_color,
nick_colors,
hi_bg_color,
debug,
window_scale,
render_api,
text_shaper,
)),
dc_key: OsRng.gen(),
mouse_pos: SyncMutex::new(Point::from([0., 0.])),
touch_info: SyncMutex::new(None),
touch_is_active: AtomicBool::new(false),
rect,
scroll,
z_index,
priority,
scroll_start_accel,
scroll_resist,
select_hold_time,
key_scroll_speed,
motion_cv,
speed: AtomicF32::new(0.),
mouse_btn_held: AtomicBool::new(false),
bgload_cv,
parent_rect: SyncMutex::new(None),
});
Pimpl::ChatView(self_)
}
async fn process_insert_line_method(me: &Weak, sub: &MethodCallSub) -> bool {
let Ok(method_call) = sub.receive().await else {
debug!(target: "ui::chatview", "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<(Timestamp, MessageId, String, String)> {
let mut cur = Cursor::new(&data);
let timestamp = Timestamp::decode(&mut cur)?;
let msg_id = MessageId::decode(&mut cur)?;
let nick = String::decode(&mut cur)?;
let text = String::decode(&mut cur)?;
Ok((timestamp, msg_id, nick, text))
}
let Ok((timestamp, msg_id, nick, text)) = decode_data(&method_call.data) else {
error!(target: "ui::chatview", "insert_line() method invalid arg data");
return true
};
let Some(self_) = me.upgrade() else {
// Should not happen
panic!("self destroyed before insert_line_method_task was stopped!");
};
self_.handle_insert_line(timestamp, msg_id, nick, text).await;
true
}
async fn process_insert_unconf_line_method(me: &Weak, sub: &MethodCallSub) -> bool {
let Ok(method_call) = sub.receive().await else {
debug!(target: "ui::chatview", "Event relayer closed");
return false
};
//debug!(target: "ui::chatview", "method called: insert_unconf_line({method_call:?})");
assert!(method_call.send_res.is_none());
fn decode_data(data: &[u8]) -> std::io::Result<(Timestamp, MessageId, String, String)> {
let mut cur = Cursor::new(&data);
let timestamp = Timestamp::decode(&mut cur)?;
let msg_id = MessageId::decode(&mut cur)?;
let nick = String::decode(&mut cur)?;
let text = String::decode(&mut cur)?;
Ok((timestamp, msg_id, nick, text))
}
let Ok((timestamp, msg_id, nick, text)) = decode_data(&method_call.data) else {
error!(target: "ui::chatview", "insert_unconf_line() method invalid arg data");
return true
};
let Some(self_) = me.upgrade() else {
// Should not happen
panic!("self destroyed before touch_task was stopped!");
};
self_.handle_insert_unconf_line(timestamp, msg_id, nick, text).await;
true
}
/// Mark line as selected
async fn select_line(&self, mut y: f32) {
// The cursor is inside the rect. We just have to find which line it clicked.
let rect = self.rect.get();
// y coord within widget's screen rect
y -= rect.y;
// The scroll is the position of the bottom of the rect on screen
let scroll = self.scroll.get();
// Now what is its distance from the absolute bottom
y = rect.h - y + scroll;
let mut msgbuf = self.msgbuf.lock().await;
msgbuf.select_line(y).await;
self.redraw_cached(&mut msgbuf).await;
}
fn end_touch_phase(&self, touch_y: f32) {
// Now calculate scroll acceleration
let touch_info = std::mem::replace(&mut *self.touch_info.lock().unwrap(), None);
let Some(touch_info) = &touch_info else { return };
self.touch_is_active.store(false, Ordering::Relaxed);
// No scroll accel with selection mode
if touch_info.is_select_mode.is_some() {
return
}
let Some((time, sample_y)) = touch_info.first_sample() else { return };
let dist = touch_y - sample_y;
// Ignore sub-ms events
if time < 1. {
error!(target: "ui::chatview", "Received a sub-ms touch event!");
return
}
//let speed = dist / time;
//self.speed.fetch_add(speed, Ordering::Relaxed);
//debug!(target: "ui::chatview", "speed = {dist} / {time} = {speed}");
let accel = self.scroll_start_accel.get() * dist / time;
let touch_time = touch_info.start_instant.elapsed();
//debug!(target: "ui::chatview", "accel = {dist} / {time} = {accel}, touch = {touch_time:?}");
self.speed.fetch_add(accel, Ordering::Relaxed);
self.motion_cv.notify();
}
async fn add_line_to_db(
&self,
timest: Timestamp,
msg_id: &MessageId,
nick: &str,
text: &str,
) -> bool {
assert!(timest > 6047051717);
let timest = timest.to_be_bytes();
assert_eq!(timest.len(), 8);
let mut key = [0u8; 8 + 32];
key[..8].clone_from_slice(×t);
key[8..].clone_from_slice(&msg_id.0);
// When does this return Err?
let contains_key = self.tree.contains_key(&key);
if contains_key.is_err() || contains_key.unwrap() {
// Already exists
return false
}
let msg = ChatMsg { nick: nick.to_string(), text: text.to_string() };
let mut val = vec![];
msg.encode(&mut val).unwrap();
self.tree.insert(&key, val).unwrap();
let _ = self.tree.flush_async().await;
true
}
pub async fn handle_insert_line(
&self,
timest: Timestamp,
msg_id: MessageId,
nick: String,
text: String,
) {
debug!(target: "ui::chatview", "handle_insert_line({timest}, {msg_id}, {nick}, {text})");
// Lock message buffer so background loader doesn't load the message as soon as it's
// inserted into the DB.
let mut msgbuf = self.msgbuf.lock().await;
if !self.add_line_to_db(timest, &msg_id, &nick, &text).await {
// Already exists so bail
debug!(target: "ui::chatview", "duplicate msg so bailing");
return
}
// Add message to page
if msgbuf.mark_confirmed(&msg_id) {
// Message already exists. Which means it must be an unconfirmed sent message.
// Mark it as confirmed.
debug!(target: "ui::chatview", "Mark sent message as confirmed");
} else {
debug!(target: "ui::chatview", "Inserting new message");
// Insert the privmsg since it doesn't already exist
if msgbuf.insert_privmsg(timest, msg_id, nick, text).is_none() {
// Not visible so no need to redraw
return
}
}
self.redraw_cached(&mut msgbuf).await;
self.bgload_cv.notify();
}
async fn handle_insert_unconf_line(
&self,
timest: Timestamp,
msg_id: MessageId,
nick: String,
text: String,
) {
debug!(target: "ui::chatview", "handle_insert_unconf_line({timest}, {msg_id}, {nick}, {text})");
// We don't add unconfirmed lines to the db. Maybe we should?
// Add message to page
let mut msgbuf = self.msgbuf.lock().await;
let Some(privmsg) = msgbuf.insert_privmsg(timest, msg_id, nick, text) else { return };
privmsg.confirmed = false;
self.redraw_cached(&mut msgbuf).await;
self.bgload_cv.notify();
}
/// Signal to begin scrolling
fn start_scroll(&self, y: f32) {
self.speed.fetch_add(y * self.scroll_start_accel.get(), Ordering::Relaxed);
self.motion_cv.notify();
}
async fn handle_movement(&self) {
// We need to fix this impl because it depends very much on the speed of the device
// that it's running on.
// Look into optimizing scrollview() so scrolling is smooth.
// We could use skiplists to avoid looping from the very bottom.
// So index 1 in the skiplist advances to 100px up... (or however much multiplier)
loop {
msleep(10).await;
if self.touch_is_active.load(Ordering::Relaxed) {
return
}
let mut speed = self.speed.load(Ordering::Relaxed);
// Apply constant decel to speed
speed *= self.scroll_resist.get();
if speed.abs() < BIG_EPSILON {
speed = 0.;
}
self.speed.store(speed, Ordering::Relaxed);
// Finished
if is_zero(speed) {
return
}
let scroll = self.scroll.get() + speed;
let dist = self.scrollview(scroll).await;
// We reached the end so just stop
if is_zero(dist) {
self.speed.store(0., Ordering::Relaxed);
return
}
}
}
async fn handle_bgload(&self) {
//debug!(target: "ui::chatview", "ChatView::handle_bgload()");
// Do we need to load some more?
let scroll = self.scroll.get();
let rect = self.rect.get();
let top = scroll + rect.h;
let preload_height = PRELOAD_PAGES as f32 * rect.h;
let mut msgbuf = self.msgbuf.lock().await;
let total_height = msgbuf.calc_total_height().await;
if total_height > top + preload_height {
// Nothing to do here
//debug!(target: "ui::chatview", "bgloader: buffer is sufficient");
return
}
// Keep loading until this is below 0
let mut remaining_load_height = top + preload_height - total_height;
//debug!(target: "ui::chatview", "bgloader: remaining px = {remaining_load_height}");
let mut remaining_visible = top - total_height;
// Get the current earliest timestamp
let iter = match msgbuf.oldest_timestamp() {
Some(oldest_timest) => {
// iterate from there
//debug!(target: "ui::chatview", "preloading from {oldest_timest}");
let timest = (oldest_timest - 1).to_be_bytes();
let mut key = [0u8; 8 + 32];
key[..8].clone_from_slice(×t);
let iter = self.tree.range(..key).rev();
iter
}
None => {
//debug!(target: "ui::chatview", "initial load");
self.tree.iter().rev()
}
};
let mut do_redraw = false;
for entry in iter {
let Ok((k, v)) = entry else { break };
assert_eq!(k.len(), 8 + 32);
let timest_bytes: [u8; 8] = k[..8].try_into().unwrap();
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:?}");
let msg_height = msgbuf.push_privmsg(timest, msg_id, chatmsg.nick, chatmsg.text);
remaining_load_height -= msg_height;
if remaining_load_height <= 0. {
break
}
// Do this once at the end rather than continuously redrawing
if remaining_visible > 0. {
do_redraw = true;
}
remaining_visible -= msg_height;
}
if do_redraw {
self.redraw_cached(&mut msgbuf).await;
}
}
async fn scrollview(&self, mut scroll: f32) -> f32 {
//debug!(target: "ui::chatview", "scrollview()");
let old_scroll = self.scroll.get();
let rect = self.rect.get();
let mut msgbuf = self.msgbuf.lock().await;
// 1/3 of time spent here ~1.5ms
if let Some(new_scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
scroll = new_scroll;
}
// 2/3 of time spent here ~3.3ms
self.redraw_cached(&mut msgbuf).await;
self.scroll.set(scroll);
self.bgload_cv.notify();
scroll - old_scroll
}
/// Adjusts a proposed scroll value to clamp it within range. It will load pages until we
/// either run out or we have enough, then checks scroll is within range.
/// Returns None if the value is within range.
async fn adjust_scroll(
&self,
msgbuf: &mut MessageBuffer,
mut scroll: f32,
rect_h: f32,
) -> Option {
// We still wish to preload pages to fill the screen, so we just adjust it up to 0.
let nonneg_scroll = max(scroll, 0.);
if scroll < 0. {
return Some(0.)
}
let total_height = msgbuf.calc_total_height().await;
let max_allowed_scroll = if total_height > rect_h { total_height - rect_h } else { 0. };
if scroll > max_allowed_scroll {
scroll = max_allowed_scroll;
assert!(scroll >= 0.);
return Some(scroll)
}
// Unchanged
None
}
/// Returns draw calls for drawing
async fn get_meshes(
&self,
msgbuf: &mut MessageBuffer,
rect: &Rectangle,
) -> Vec {
let scroll = self.scroll.get();
let total_height = msgbuf.calc_total_height().await;
// Use this to start from the top
//let start_pos = if total_height < rect.h { total_height } else { rect.h };
// We start from the bottom though
let start_pos = rect.h;
let mut instrs = vec![];
//let mut old_drawmesh = vec![];
let meshes = msgbuf.gen_meshes(rect, scroll).await;
for (i, (y_pos, mesh)) in enumerate(meshes) {
// Apply scroll and scissor
// We use the scissor for scrolling
// Because we use the scissor, our actual rect is now rect instead of parent_rect
let off_x = 0.;
// This calc decides whether scroll is in terms of pages or pixels
let off_y = (scroll + start_pos - y_pos);
let pos = Point::from([0., off_y]);
instrs.push(GfxDrawInstruction::Move(pos));
instrs.push(GfxDrawInstruction::Draw(mesh));
}
instrs
}
async fn redraw_cached(&self, msgbuf: &mut MessageBuffer) {
//debug!(target: "ui::chatview", "ChatView::redraw_cached()");
let timest = unixtime();
let rect = self.rect.get();
let mut mesh_instrs = self.get_meshes(msgbuf, &rect).await;
let mut instrs = vec![GfxDrawInstruction::ApplyView(rect)];
instrs.append(&mut mesh_instrs);
let draw_calls =
vec![(self.dc_key, GfxDrawCall { instrs, dcs: vec![], z_index: self.z_index.get() })];
self.render_api.replace_draw_calls(timest, draw_calls);
//debug!(target: "ui::chatview", "ChatView::redraw_cached() DONE");
}
/// Invalidates cache and redraws everything
async fn redraw_all(&self) {
//debug!(target: "ui::chatview", "ChatView::redraw_all()");
let parent_rect = self.parent_rect.lock().unwrap().unwrap().clone();
self.rect.eval(&parent_rect).expect("unable to eval rect");
let mut msgbuf = self.msgbuf.lock().await;
msgbuf.adjust_params();
msgbuf.clear_meshes();
self.redraw_cached(&mut msgbuf).await;
//debug!(target: "ui::chatview", "ChatView::redraw_all() DONE");
}
}
#[async_trait]
impl UIObject for ChatView {
fn priority(&self) -> u32 {
self.priority.get()
}
async fn start(self: Arc, 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 method_sub = node_ref.subscribe_method_call("insert_line").unwrap();
let me2 = me.clone();
let insert_line_method_task =
ex.spawn(
async move { while Self::process_insert_line_method(&me2, &method_sub).await {} },
);
let method_sub = node_ref.subscribe_method_call("insert_unconf_line").unwrap();
let me2 = me.clone();
let insert_unconf_line_method_task = ex.spawn(async move {
while Self::process_insert_unconf_line_method(&me2, &method_sub).await {}
});
let me2 = me.clone();
let cv = self.motion_cv.clone();
let motion_task = ex.spawn(async move {
loop {
cv.wait().await;
let Some(self_) = me2.upgrade() else {
// Should not happen
panic!("self destroyed before motion_task was stopped!");
};
self_.handle_movement().await;
cv.reset();
}
});
let me2 = me.clone();
let cv = self.bgload_cv.clone();
let bgload_task = ex.spawn(async move {
loop {
cv.wait().await;
let Some(self_) = me2.upgrade() else {
// Should not happen
panic!("self destroyed before bgload_task was stopped!");
};
self_.handle_bgload().await;
cv.reset();
}
});
let mut on_modify = OnModify::new(ex, node_name, node_id, me.clone());
async fn reload_view(self_: Arc) {
self_.scrollview(self_.scroll.get()).await;
}
on_modify.when_change(self.scroll.prop(), reload_view);
async fn redraw(self_: Arc) {
if !self_.rect.has_cached() {
return
}
self_.redraw_all().await;
}
//on_modify.when_change(self.baseline.prop(), redraw);
//on_modify.when_change(self.font_size.prop(), redraw);
//on_modify.when_change(self.timestamp_font_size.prop(), redraw);
//on_modify.when_change(self.timestamp_color.prop(), redraw);
//on_modify.when_change(self.timestamp_width.prop(), redraw);
//on_modify.when_change(self.line_height.prop(), redraw);
//on_modify.when_change(self.message_spacing.prop(), redraw);
//on_modify.when_change(self.text_color.prop(), redraw);
//on_modify.when_change(self.nick_colors.clone(), redraw);
//on_modify.when_change(self.hi_bg_color.prop(), redraw);
on_modify.when_change(self.rect.prop(), redraw);
//on_modify.when_change(self.debug.prop(), redraw);
let mut tasks =
vec![insert_line_method_task, insert_unconf_line_method_task, motion_task, bgload_task];
tasks.append(&mut on_modify.tasks);
self.tasks.set(tasks);
}
async fn draw(&self, parent_rect: Rectangle) -> Option {
debug!(target: "ui::chatview", "ChatView::draw({:?})", self.node.upgrade().unwrap());
*self.parent_rect.lock().unwrap() = Some(parent_rect.clone());
self.rect.eval(&parent_rect).ok()?;
let rect = self.rect.get();
let mut msgbuf = self.msgbuf.lock().await;
msgbuf.adjust_window_scale();
msgbuf.adjust_width(rect.w);
msgbuf.clear_meshes();
let mut scroll = self.scroll.get();
if let Some(scroll) = self.adjust_scroll(&mut msgbuf, scroll, rect.h).await {
self.scroll.set(scroll);
}
// We may need to load more messages since the screen size has changed.
// Now we have updated all the values so it's safe to wake up here.
self.bgload_cv.notify();
let mut mesh_instrs = self.get_meshes(&mut msgbuf, &rect).await;
drop(msgbuf);
let mut instrs = vec![GfxDrawInstruction::ApplyView(rect)];
instrs.append(&mut mesh_instrs);
Some(DrawUpdate {
key: self.dc_key,
draw_calls: vec![(
self.dc_key,
GfxDrawCall { instrs, dcs: vec![], z_index: self.z_index.get() },
)],
})
}
async fn handle_key_down(&self, key: KeyCode, mods: KeyMods, repeat: bool) -> bool {
if repeat {
return false
}
match key {
KeyCode::PageUp => {
self.start_scroll(1. * self.key_scroll_speed.get());
return true
}
KeyCode::PageDown => {
self.start_scroll(-1. * self.key_scroll_speed.get());
return true
}
_ => {}
}
false
}
async fn handle_mouse_btn_down(&self, btn: MouseButton, mouse_pos: Point) -> bool {
if btn != MouseButton::Left {
return false
}
let rect = self.rect.get();
if !rect.contains(mouse_pos) {
return false
}
if ENABLE_SELECT {
self.select_line(mouse_pos.y).await;
}
self.mouse_btn_held.store(true, Ordering::Relaxed);
true
}
async fn handle_mouse_btn_up(&self, btn: MouseButton, mouse_pos: Point) -> bool {
if btn != MouseButton::Left {
return false
}
self.mouse_btn_held.store(false, Ordering::Relaxed);
false
}
async fn handle_mouse_move(&self, mouse_pos: Point) -> bool {
//debug!(target: "ui::chatview", "handle_mouse_move({mouse_x}, {mouse_y})");
// We store the mouse pos for use in handle_mouse_wheel()
*self.mouse_pos.lock().unwrap() = mouse_pos.clone();
if !self.mouse_btn_held.load(Ordering::Relaxed) {
return false
}
let rect = self.rect.get();
if !rect.contains(mouse_pos) {
return false
}
if ENABLE_SELECT {
self.select_line(mouse_pos.y).await;
}
false
}
async fn handle_mouse_wheel(&self, wheel_pos: Point) -> bool {
//debug!(target: "ui::chatview", "handle_mouse_wheel({wheel_x}, {wheel_y})");
let rect = self.rect.get();
let mouse_pos = self.mouse_pos.lock().unwrap().clone();
if !rect.contains(mouse_pos) {
//debug!(target: "ui::chatview", "not inside rect");
return false
}
self.start_scroll(wheel_pos.y);
true
}
async fn handle_touch(&self, phase: TouchPhase, id: u64, touch_pos: Point) -> bool {
// Ignore multi-touch
if id != 0 {
return false
}
let rect = self.rect.get();
//debug!(target: "ui::chatview", "handle_touch({phase:?}, {touch_x}, {touch_y})");
let touch_y = touch_pos.y;
if !rect.contains(touch_pos) {
match phase {
TouchPhase::Started => *self.touch_info.lock().unwrap() = None,
_ => self.end_touch_phase(touch_y),
}
return false
}
let select_hold_time = self.select_hold_time.get();
// Simulate mouse events
match phase {
TouchPhase::Started => {
self.touch_is_active.store(true, Ordering::Relaxed);
let mut touch_info = self.touch_info.lock().unwrap();
*touch_info = Some(TouchInfo::new(self.scroll.get(), touch_y));
}
TouchPhase::Moved => {
let (start_scroll, start_y, start_elapsed, do_update, is_select_mode) = {
let mut touch_info = self.touch_info.lock().unwrap();
let Some(touch_info) = &mut *touch_info else { return false };
touch_info.last_y = touch_y;
let start_scroll = touch_info.start_scroll;
let start_y = touch_info.start_y;
let start_elapsed = touch_info.start_instant.elapsed().as_millis_f32();
if start_elapsed > select_hold_time && touch_info.is_select_mode.is_none() {
// Did we move?
if (touch_y - start_y).abs() < BIG_EPSILON {
touch_info.is_select_mode = Some(true);
} else {
touch_info.is_select_mode = Some(false);
}
}
let is_select_mode = touch_info.is_select_mode.clone();
touch_info.push_sample(touch_y);
// Only update screen every 20ms. Avoid wasting cycles.
let last_elapsed = touch_info.last_instant.elapsed().as_millis_f32();
let do_update = last_elapsed > 20.;
if do_update {
touch_info.last_instant = std::time::Instant::now();
}
(start_scroll, start_y, start_elapsed, do_update, is_select_mode)
};
//debug!(target: "ui::chatview", "touch phase moved, is_select_mode={is_select_mode:?}");
// When scrolling if we suddenly grab the screen for more than a brief period
// of time then stop the scrolling completely.
if start_elapsed > 200. {
//debug!(target: "ui::chatview", "Stopping scroll accel");
self.speed.store(0., Ordering::Relaxed);
}
// Only update every so often to prevent wasting resources.
if !do_update {
return true
}
// We are in selection mode so don't scroll the screen until touch phase ends.
if let Some(is_select_mode) = is_select_mode &&
is_select_mode
{
if ENABLE_SELECT {
self.select_line(touch_y).await;
}
return true
}
let dist = touch_y - start_y;
// No movement so just return
if dist.abs() < BIG_EPSILON {
return true
}
let scroll = start_scroll + dist;
// Redraws the screen from the cache
self.scrollview(scroll).await;
}
TouchPhase::Ended | TouchPhase::Cancelled => {
self.end_touch_phase(touch_y);
}
}
true
}
}
impl Drop for ChatView {
fn drop(&mut self) {
self.render_api.replace_draw_calls(unixtime(), vec![(self.dc_key, Default::default())]);
}
}