From 30d1184edb6d0472cb83c0bf9cc4410aeb8545eb Mon Sep 17 00:00:00 2001 From: darkfi Date: Mon, 19 Aug 2024 21:03:56 +0200 Subject: [PATCH] wallet: begin making a PageManager for chatview --- bin/darkwallet/src/app.rs | 6 +- .../src/ui/{chatview.rs => chatview/mod.rs} | 15 +- bin/darkwallet/src/ui/chatview/page.rs | 282 ++++++++++++++++++ 3 files changed, 297 insertions(+), 6 deletions(-) rename bin/darkwallet/src/ui/{chatview.rs => chatview/mod.rs} (98%) create mode 100644 bin/darkwallet/src/ui/chatview/page.rs diff --git a/bin/darkwallet/src/app.rs b/bin/darkwallet/src/app.rs index c7015fcc9..79200b2dd 100644 --- a/bin/darkwallet/src/app.rs +++ b/bin/darkwallet/src/app.rs @@ -206,9 +206,9 @@ impl App { self.trigger_redraw().await; // Start the backend - if let Err(err) = self.darkirc_backend.start(self.sg.clone(), self.ex.clone()).await { - error!(target: "app", "backend error: {err}"); - } + //if let Err(err) = self.darkirc_backend.start(self.sg.clone(), self.ex.clone()).await { + // error!(target: "app", "backend error: {err}"); + //} } pub fn stop(&self) { diff --git a/bin/darkwallet/src/ui/chatview.rs b/bin/darkwallet/src/ui/chatview/mod.rs similarity index 98% rename from bin/darkwallet/src/ui/chatview.rs rename to bin/darkwallet/src/ui/chatview/mod.rs index 3930a869e..027aa57a1 100644 --- a/bin/darkwallet/src/ui/chatview.rs +++ b/bin/darkwallet/src/ui/chatview/mod.rs @@ -31,6 +31,8 @@ use std::{ sync::{atomic::Ordering, Arc, Mutex as SyncMutex, Weak}, }; +mod page; + use crate::{ gfx::{ DrawCall, DrawInstruction, DrawMesh, GraphicsEventPublisherPtr, Point, Rectangle, @@ -257,6 +259,7 @@ pub struct ChatView { tree: sled::Tree, pages: AsyncMutex>, + pages2: AsyncMutex, dc_key: u64, /// Used for detecting when scrolling view @@ -389,11 +392,12 @@ impl ChatView { node_id, tasks, sg, - render_api, - text_shaper, + render_api: render_api.clone(), + text_shaper: text_shaper.clone(), tree, pages: AsyncMutex::new(Vec::new()), + pages2: AsyncMutex::new(page::PageManager::new(render_api, text_shaper)), dc_key: OsRng.gen(), mouse_pos: SyncMutex::new(Point::from([0., 0.])), @@ -662,6 +666,11 @@ impl ChatView { return } + // Add message to page + self.pages2.lock().await + .insert_line(self.font_size.get(), timest, message_id, nick.clone(), text.clone()) + .await; + let chatmsg = ChatMsg { nick, text }; let dt = Local.timestamp_opt(timest as i64, 0).unwrap(); @@ -973,12 +982,12 @@ impl ChatView { while total_height < *scroll + rect.h { debug!(target: "ui::chatview", "draw_cached() loading more pages"); - // No more pages available to load let n_loaded_pages = self.preload_pages().await; // We need this value after so first update it total_height = self.get_total_height(&rect, &pages).await; + // No more pages available to load if n_loaded_pages == 0 { break } diff --git a/bin/darkwallet/src/ui/chatview/page.rs b/bin/darkwallet/src/ui/chatview/page.rs new file mode 100644 index 000000000..788b04d2a --- /dev/null +++ b/bin/darkwallet/src/ui/chatview/page.rs @@ -0,0 +1,282 @@ +/* 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 chrono::{Local, TimeZone}; +use miniquad::{BufferId, TextureId}; +use std::{ + hash::{DefaultHasher, Hash, Hasher}, + io::Cursor, + sync::{atomic::Ordering, Arc, Mutex as SyncMutex, Weak}, +}; + +use crate::{ + gfx::{ + DrawCall, DrawInstruction, DrawMesh, GraphicsEventPublisherPtr, Point, Rectangle, + RenderApi, RenderApiPtr, + }, + mesh::{Color, MeshBuilder, COLOR_BLUE, COLOR_GREEN}, + prop::{PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr, PropertyUint32, Role}, + pubsub::Subscription, + scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId}, + text::{self, Glyph, GlyphPositionIter, TextShaper, TextShaperPtr}, + ExecutorPtr, +}; + +const PAGE_SIZE: usize = 10; +const PRELOAD_PAGES: usize = 10; + +type Timestamp = u64; +type MessageId = [u8; 32]; + +#[derive(Clone)] +struct Message { + font_size: f32, + + timestamp: Timestamp, + id: MessageId, + nick: String, + text: String, + + unwrapped_glyphs: Vec, + wrapped_lines: Vec>, + line_width: f32, +} + +impl Message { + async fn new( + font_size: f32, + + timestamp: Timestamp, + id: MessageId, + nick: String, + text: String, + + text_shaper: &TextShaper, + ) -> Self { + let dt = Local.timestamp_opt(timestamp as i64, 0).unwrap(); + let timestr = dt.format("%H:%M").to_string(); + + let linetext = format!("{} {} {}", timestr, nick, text); + let unwrapped_glyphs = text_shaper.shape(linetext, font_size).await; + + Self { + font_size, + timestamp, + id, + nick, + text, + unwrapped_glyphs, + wrapped_lines: vec![], + line_width: 0., + } + } + + fn adjust_line_width(&mut self, line_width: f32) { + if (line_width - self.line_width).abs() < f32::EPSILON { + return; + } + + // Invalidate wrapped_glyphs and recalc + self.wrapped_lines = text::wrap(line_width, self.font_size, &self.unwrapped_glyphs); + self.line_width = line_width; + } +} + +#[derive(Clone)] +struct PageMeshInfo { + px_height: f32, + mesh: DrawMesh, +} + +struct Page { + msgs: Vec, + atlas: text::RenderedAtlas, + mesh: Option, +} + +impl Page { + async fn new(msgs: Vec, render_api: &RenderApi) -> Self { + let mut atlas = text::Atlas::new(render_api); + for msg in &msgs { + atlas.push(&msg.unwrapped_glyphs); + } + let atlas = atlas.make().await.expect("unable to make atlas!"); + + Self { msgs, atlas, mesh: None } + } + + fn invalidate(&mut self) -> Option { + std::mem::replace(&mut self.mesh, None).map(|m| m.mesh) + } + + async fn split(mut msgs: Vec, render_api: &RenderApi) -> Vec { + msgs.sort_unstable_by_key(|msg| msg.timestamp); + msgs.reverse(); + + let chunk_size = if msgs.len() > PAGE_SIZE { + // Round up so we don't get a weird page with a single item + msgs.len() / 2 + 1 + } else { + PAGE_SIZE + }; + + // Replace single page with N pages each with chunk_size messages + let mut new_pages = vec![]; + for page_msgs in msgs.chunks(chunk_size).map(|m| m.to_vec()) { + debug!(target: "ui::chatview", "PAGE =========================="); + for msg in &page_msgs { + debug!(target: "ui::chatview", "{} {:?}", msg.timestamp, msg.text); + } + debug!(target: "ui::chatview", "==============================="); + + let new_page = Page::new(page_msgs, render_api).await; + new_pages.push(new_page); + } + assert!(!new_pages.is_empty()); + assert!(new_pages.len() <= 2); + new_pages + } +} + +struct FreedData { + buffers: Vec, + textures: Vec, +} + +impl FreedData { + fn add(&mut self, mesh: DrawMesh) { + self.buffers.push(mesh.vertex_buffer); + self.buffers.push(mesh.index_buffer); + if let Some(texture_id) = mesh.texture { + self.textures.push(texture_id); + } + } +} + +pub struct PageManager { + pages: Vec, + freed: FreedData, + render_api: RenderApiPtr, + text_shaper: TextShaperPtr, +} + +impl PageManager { + pub fn new( + render_api: RenderApiPtr, + text_shaper: TextShaperPtr) -> Self { + Self { pages: vec![], freed: FreedData { buffers: vec![], textures: vec![] }, render_api, text_shaper } + } + + /// For scrolling we want to be able to adjust and measure without + /// explicitly rendering since it may be off screen. + fn adjust_line_width(&mut self, line_width: f32) { + for page in &mut self.pages { + for msg in &mut page.msgs { + msg.adjust_line_width(line_width); + } + } + } + + fn calc_total_height(&self) -> f32 { + 0. + } + + pub(super) async fn insert_line( + &mut self, + font_size: f32, + timestamp: Timestamp, + message_id: MessageId, + nick: String, + text: String, + ) { + let msg = + Message::new(font_size, timestamp, message_id, nick, text, &self.text_shaper).await; + + // Now add message to page + + // Maybe we can write this code below better + if self.pages.is_empty() { + let page = Page::new(vec![msg], &self.render_api).await; + self.pages.push(page); + return; + } + + let mut idx = None; + for (i, page) in self.pages.iter_mut().enumerate() { + //let first_timest = page.msgs.last().unwrap().timest; + let last_timest = page.msgs.first().unwrap().timestamp; + + //debug!(target: "ui::chatview", "page {i} [{first_timest}, {last_timest}]"); + if timestamp <= last_timest { + //debug!(target: "ui::chatview", "found page {i} [{first_timest}, {last_timest}]"); + idx = Some(i); + break + } + } + + let Some(idx) = idx else { + // Add to the end + let page = Page::new(vec![msg], &self.render_api).await; + self.pages.push(page); + return + }; + + let old_pages_len = self.pages.len(); + // We now want to replace this page by 1 or 2 pages + // Split pages into 3 parts: head, replaced page and tail + let mut head = std::mem::replace(&mut self.pages, vec![]); + let mut drain_iter = head.drain(idx..); + // Drop the item at idx which will be replaced + let old_page = drain_iter.next().unwrap(); + let mut tail: Vec<_> = drain_iter.collect(); + assert_eq!(old_pages_len, head.len() + 1 + tail.len()); + + let mut msgs = old_page.msgs; + msgs.push(msg); + let mut new_pages = Page::split(msgs, &self.render_api).await; + + self.pages.append(&mut head); + self.pages.append(&mut new_pages); + self.pages.append(&mut tail); + } + + /// Clear all meshes and caches. Returns data that needs to be freed. + fn invalidate_caches(&mut self) { + for page in &mut self.pages { + if let Some(mesh) = page.invalidate() { + self.freed.add(mesh); + } + } + } + + /// Generate caches and return meshes + pub(super) async fn get_meshes( + &self, + clip: &Rectangle, + render_api: &RenderApi, + font_size: f32, + line_height: f32, + baseline: f32, + nick_colors: &[Color], + timestamp_color: Color, + text_color: Color, + debug_render: bool, + ) { + } +}