diff --git a/bin/darkwallet/src/mesh.rs b/bin/darkwallet/src/mesh.rs index 1821a5a08..65753a8b7 100644 --- a/bin/darkwallet/src/mesh.rs +++ b/bin/darkwallet/src/mesh.rs @@ -18,9 +18,9 @@ use crate::{ error::Result, - gfx2::{Point, Rectangle, RenderApi, Vertex}, + gfx2::{DrawMesh, Point, Rectangle, RenderApi, Vertex}, }; -use miniquad::BufferId; +use miniquad::{BufferId, TextureId}; pub type Color = [f32; 4]; @@ -40,6 +40,27 @@ pub struct MeshInfo { pub num_elements: i32, } +impl MeshInfo { + /// Convenience method + pub fn draw_with_texture(self, texture: TextureId) -> DrawMesh { + DrawMesh { + vertex_buffer: self.vertex_buffer, + index_buffer: self.index_buffer, + texture: Some(texture), + num_elements: self.num_elements, + } + } + /// Convenience method + pub fn draw_untextured(self) -> DrawMesh { + DrawMesh { + vertex_buffer: self.vertex_buffer, + index_buffer: self.index_buffer, + texture: None, + num_elements: self.num_elements, + } + } +} + pub struct MeshBuilder { verts: Vec, indices: Vec, diff --git a/bin/darkwallet/src/ui/chatview.rs b/bin/darkwallet/src/ui/chatview.rs index 22c7f9b50..898cb6d7a 100644 --- a/bin/darkwallet/src/ui/chatview.rs +++ b/bin/darkwallet/src/ui/chatview.rs @@ -65,7 +65,7 @@ struct Message { } const LINES_PER_PAGE: usize = 10; -const PRELOAD_PAGES: usize = 200; +const PRELOAD_PAGES: usize = 10; #[derive(Clone)] struct Page { @@ -73,6 +73,122 @@ struct Page { atlas: text2::RenderedAtlas, } +#[derive(Clone)] +struct PageMeshInfo { + px_height: f32, + mesh: DrawMesh, +} + +type Page2Ptr = Arc; + +struct Page2 { + msgs: Vec, + atlas: text2::RenderedAtlas, + // One draw call per page. + // Resizing the canvas means we recalc wrapping and the mesh changes + mesh_inf: SyncMutex>, +} + +impl Page2 { + async fn new(msgs: Vec, render_api: &RenderApi) -> Arc { + let mut atlas = text2::Atlas::new(render_api); + for msg in &msgs { + atlas.push(&msg.glyphs); + } + let Ok(atlas) = atlas.make().await else { + // what else should I do here? + panic!("unable to make atlas!"); + }; + + Arc::new(Self { msgs, atlas, mesh_inf: SyncMutex::new(None) }) + } + + /// Regenerates the mesh, returning the old mesh which should be freed + async fn regen_mesh( + &self, + clip: &Rectangle, + render_api: &RenderApi, + font_size: f32, + line_height: f32, + baseline: f32, + nick_colors: &[Color], + timestamp_color: Color, + text_color: Color, + ) -> (PageMeshInfo, Option) { + // Nudge the bottom line up slightly, otherwise chars like p will cross the bottom. + //let descent = line_height - baseline; + + let mut wrapped_line_idx = 0; + + let mut mesh = MeshBuilder::new(); + + for msg in &self.msgs { + let glyphs = &msg.glyphs; + + let nick_color = select_nick_color(&msg.chatmsg.nick, nick_colors); + + // Keep track of the 'section' + // Section 0 is the timestamp + // Section 1 is the nickname (colorized) + // Finally is just the message itself + let mut section = 2; + + let mut lines = text2::wrap(clip.w, font_size, glyphs); + // We are drawing bottom up but line wrap gives us lines in normal order + lines.reverse(); + let last_idx = lines.len() - 1; + for (i, line) in lines.into_iter().enumerate() { + let off_y = wrapped_line_idx as f32 * line_height; + + if i == last_idx { + section = 0; + } + + // Render line + let mut glyph_pos_iter = GlyphPositionIter::new(font_size, &line, baseline); + for (mut glyph_rect, glyph) in glyph_pos_iter.zip(line.iter()) { + let uv_rect = + self.atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect"); + glyph_rect.y -= off_y; + + let color = match section { + 0 => timestamp_color, + 1 => nick_color, + _ => text_color, + }; + + mesh.draw_box(&glyph_rect, color, uv_rect); + + if section < 2 && is_whitespace(&glyph.substr) { + section += 1; + } + } + wrapped_line_idx += 1; + } + } + + let px_height = wrapped_line_idx as f32 * line_height; + + let mesh = mesh.alloc(render_api).await.unwrap(); + let mesh = mesh.draw_with_texture(self.atlas.texture_id); + + let mesh_inf = PageMeshInfo { px_height, mesh }; + + let old = std::mem::replace(&mut *self.mesh_inf.lock().unwrap(), Some(mesh_inf.clone())); + let old = old.map(|v| v.mesh); + + (mesh_inf, old) + } +} + +fn select_nick_color(nick: &str, nick_colors: &[Color]) -> Color { + let mut hasher = DefaultHasher::new(); + nick.hash(&mut hasher); + let i = hasher.finish() as usize; + let color = nick_colors[i % nick_colors.len()]; + color +} + struct TouchInfo { start_scroll: f32, start_y: f32, @@ -97,6 +213,7 @@ pub struct ChatView { tree: sled::Tree, pages: SyncMutex>, + pages2: SyncMutex>, drawcalls: SyncMutex>, dc_key: u64, @@ -189,6 +306,7 @@ impl ChatView { tree, pages: SyncMutex::new(Vec::new()), + pages2: SyncMutex::new(Vec::new()), drawcalls: SyncMutex::new(Vec::new()), dc_key: OsRng.gen(), @@ -207,7 +325,9 @@ impl ChatView { } }); + let timer = std::time::Instant::now(); self_.populate().await; + debug!(target: "ui::chatview", "populate() took {:?}", timer.elapsed()); Pimpl::ChatView(self_) } @@ -261,10 +381,10 @@ impl ChatView { let mouse_pos = self.mouse_pos.lock().unwrap().clone(); if !rect.contains(&mouse_pos) { - debug!(target: "ui::chatview", "not inside rect"); + //debug!(target: "ui::chatview", "not inside rect"); return } - debug!(target: "ui::chatview", "inside rect"); + //debug!(target: "ui::chatview", "inside rect"); let mut scroll = self.scroll.get() + wheel_y * 50.; if scroll < 0. { scroll = 0.; @@ -353,6 +473,8 @@ impl ChatView { async fn populate(&self) { let mut pages = vec![]; + let mut pages_len = 0; + let mut msgs = vec![]; for entry in self.tree.iter().rev() { @@ -374,19 +496,12 @@ impl ChatView { msgs.push(Message { timest, chatmsg, glyphs }); if msgs.len() >= LINES_PER_PAGE { - let mut atlas = text2::Atlas::new(&self.render_api); - for msg in &msgs { - atlas.push(&msg.glyphs); - } - let Ok(atlas) = atlas.make().await else { - // what else should I do here? - panic!("unable to make atlas!"); - }; + let page = Page2::new(msgs.clone(), &self.render_api).await; - let page = Page { msgs: std::mem::take(&mut msgs), atlas }; - pages.push(page); + self.pages2.lock().unwrap().push(page); + pages_len += 1; - if pages.len() >= PRELOAD_PAGES { + if pages_len >= PRELOAD_PAGES { break } } @@ -395,6 +510,69 @@ impl ChatView { *self.pages.lock().unwrap() = pages; } + async fn draw_cached(&self, mut rect: Rectangle) -> Vec { + let font_size = self.font_size.get(); + let line_height = self.line_height.get(); + let baseline = self.baseline.get(); + + let mut instrs = vec![]; + + let timest_color = self.timestamp_color.get(); + let text_color = self.text_color.get(); + let nick_colors = self.read_nick_colors(); + + // Nudge the bottom line up slightly, otherwise chars like p will cross the bottom. + let descent = line_height - baseline; + + let pages = self.pages2.lock().unwrap().clone(); + let mut current_height = descent; + for page in pages { + if current_height > rect.h { + break + } + + let mesh_inf = page.mesh_inf.lock().unwrap().clone(); + let mesh_inf = match mesh_inf { + Some(mesh_inf) => mesh_inf, + None => { + let (mesh_inf, old_drawmesh) = page + .regen_mesh( + &rect, + &self.render_api, + font_size, + line_height, + baseline, + &nick_colors, + timest_color.clone(), + text_color.clone(), + ) + .await; + assert!(old_drawmesh.is_none()); + mesh_inf + } + }; + + // 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 = (self.scroll.get() - current_height + rect.h) / rect.h; + let scale_x = 1. / rect.w; + let scale_y = 1. / rect.h; + let model = glam::Mat4::from_translation(glam::Vec3::new(off_x, off_y, 0.)) * + glam::Mat4::from_scale(glam::Vec3::new(scale_x, scale_y, 1.)); + + instrs.push(DrawInstruction::ApplyMatrix(model)); + + instrs.push(DrawInstruction::Draw(mesh_inf.mesh)); + + current_height += mesh_inf.px_height; + } + + instrs + } + /// Basically a version of redraw() where regen_mesh() is never called. /// Instead we use the cached version. async fn scrollview(&self) { @@ -414,34 +592,14 @@ impl ChatView { panic!("Node {:?} bad rect property", node); }; - //let mut drawcalls = self.regen_mesh(rect.clone()).await; - - debug!(target: "ui::chatview", "chatview rect = {:?}", rect); - - // 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 = (self.scroll.get() + rect.h) / rect.h; - let scale_x = 1. / rect.w; - let scale_y = 1. / rect.h; - let model = glam::Mat4::from_translation(glam::Vec3::new(off_x, off_y, 0.)) * - glam::Mat4::from_scale(glam::Vec3::new(scale_x, scale_y, 1.)); - - let mut instrs = - vec![DrawInstruction::ApplyViewport(rect), DrawInstruction::ApplyMatrix(model)]; - let drawcalls = self.drawcalls.lock().unwrap().clone(); - let mut drawcalls: Vec<_> = - drawcalls.into_iter().map(|dc| DrawInstruction::Draw(dc)).collect(); - instrs.append(&mut drawcalls); + let mut mesh_instrs = self.draw_cached(rect.clone()).await; + let mut instrs = vec![DrawInstruction::ApplyViewport(rect)]; + instrs.append(&mut mesh_instrs); let draw_calls = vec![(self.dc_key, DrawCall { instrs, dcs: vec![], z_index: self.z_index.get() })]; self.render_api.replace_draw_calls(draw_calls).await; - - debug!(target: "ui::chatview", "scrollview done"); } async fn redraw(&self) { @@ -467,91 +625,62 @@ impl ChatView { } } - async fn regen_mesh(&self, mut clip: Rectangle) -> Vec { + async fn regen_mesh(&self, mut rect: Rectangle) -> (Vec, Vec) { let font_size = self.font_size.get(); let line_height = self.line_height.get(); let baseline = self.baseline.get(); - // Draw time and nick, then go over each word. If word crosses end of line - // then apply a line break before the word and continue. - let pages = self.pages.lock().unwrap().clone(); - let mut draws = vec![]; - let color = COLOR_WHITE; + let mut instrs = vec![]; + let mut old_drawmesh = vec![]; - let timestamp_color = self.timestamp_color.get(); + let timest_color = self.timestamp_color.get(); let text_color = self.text_color.get(); let nick_colors = self.read_nick_colors(); - // This is a little hack to nudge the bottom line up slightly, otherwise - // chars like p will cross the bottom. - let descent = baseline / 2.; + // Nudge the bottom line up slightly, otherwise chars like p will cross the bottom. + let descent = line_height - baseline; - // Pages start at the bottom. - let mut current_idx = 0; + let pages = self.pages2.lock().unwrap().clone(); + let mut current_height = descent; for page in pages { - let page_off_y = descent + baseline + current_idx as f32 * line_height; - if page_off_y > clip.h { + if current_height > rect.h { break } - let mut mesh = MeshBuilder::new(); + let (mesh_inf, old_drawmesh) = page + .regen_mesh( + &rect, + &self.render_api, + font_size, + line_height, + baseline, + &nick_colors, + timest_color.clone(), + text_color.clone(), + ) + .await; - for msg in page.msgs { - let glyphs = msg.glyphs; + // 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 = (self.scroll.get() - current_height + rect.h) / rect.h; + let scale_x = 1. / rect.w; + let scale_y = 1. / rect.h; + let model = glam::Mat4::from_translation(glam::Vec3::new(off_x, off_y, 0.)) * + glam::Mat4::from_scale(glam::Vec3::new(scale_x, scale_y, 1.)); - let nick_color = self.select_nick_color(&msg.chatmsg.nick, &nick_colors); + instrs.push(DrawInstruction::ApplyMatrix(model)); - // Keep track of the 'section' - // Section 0 is the timestamp - // Section 1 is the nickname (colorized) - // Finally is just the message itself - let mut section = 2; + instrs.push(DrawInstruction::Draw(mesh_inf.mesh)); - let mut lines = text2::wrap(clip.w, font_size, &glyphs); - // We are drawing bottom up but line wrap gives us lines in normal order - lines.reverse(); - let last_idx = lines.len() - 1; - for (i, line) in lines.into_iter().enumerate() { - let off_y = descent + baseline + current_idx as f32 * line_height; - - if i == last_idx { - section = 0; - } - - // Render line - let mut glyph_pos_iter = GlyphPositionIter::new(font_size, &line, baseline); - for (mut glyph_rect, glyph) in glyph_pos_iter.zip(line.iter()) { - let uv_rect = - page.atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect"); - glyph_rect.y -= off_y; - - let color = match section { - 0 => timestamp_color, - 1 => nick_color, - _ => text_color, - }; - - mesh.draw_box(&glyph_rect, color, uv_rect); - - if section < 2 && is_whitespace(&glyph.substr) { - section += 1; - } - } - - current_idx += 1; - } - } - - let mesh = mesh.alloc(&self.render_api).await.unwrap(); - - draws.push(DrawMesh { - vertex_buffer: mesh.vertex_buffer, - index_buffer: mesh.index_buffer, - texture: Some(page.atlas.texture_id), - num_elements: mesh.num_elements, - }); + current_height += mesh_inf.px_height; } + (instrs, old_drawmesh) + + /* if DEBUG_RENDER { let mut debug_mesh = MeshBuilder::new(); debug_mesh.draw_outline( @@ -569,6 +698,7 @@ impl ChatView { } draws + */ } fn read_nick_colors(&self) -> Vec { @@ -606,43 +736,24 @@ impl ChatView { panic!("Node {:?} bad rect property", node); }; - // TODO: Do we need this? Because of the viewport clipping - rect.x += parent_rect.x; - rect.y += parent_rect.y; - - let drawcalls = self.regen_mesh(rect.clone()).await; - let old_drawcalls = - std::mem::replace(&mut *self.drawcalls.lock().unwrap(), drawcalls.clone()); - let mut drawcalls: Vec<_> = - drawcalls.into_iter().map(|dc| DrawInstruction::Draw(dc)).collect(); + let timer = std::time::Instant::now(); + let (mut mesh_instrs, mut old_drawmesh) = self.regen_mesh(rect.clone()).await; + debug!(target: "ui::chatview", "regen_mesh() took {:?}", timer.elapsed()); let mut freed_textures = vec![]; let mut freed_buffers = vec![]; - for old_dc in old_drawcalls { - freed_buffers.push(old_dc.vertex_buffer); - freed_buffers.push(old_dc.index_buffer); - if let Some(texture_id) = old_dc.texture { - freed_textures.push(texture_id); - } + for old_mesh in old_drawmesh { + freed_buffers.push(old_mesh.vertex_buffer); + freed_buffers.push(old_mesh.index_buffer); + //if let Some(texture_id) = old_dc.texture { + // freed_textures.push(texture_id); + //} } debug!(target: "ui::chatview", "chatview rect = {:?}", rect); - // 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 = (self.scroll.get() + rect.h) / rect.h; - let scale_x = 1. / rect.w; - let scale_y = 1. / rect.h; - let model = glam::Mat4::from_translation(glam::Vec3::new(off_x, off_y, 0.)) * - glam::Mat4::from_scale(glam::Vec3::new(scale_x, scale_y, 1.)); - - let mut instrs = - vec![DrawInstruction::ApplyViewport(rect), DrawInstruction::ApplyMatrix(model)]; - //let mut instrs = vec![DrawInstruction::ApplyMatrix(model)]; - instrs.append(&mut drawcalls); + let mut instrs = vec![DrawInstruction::ApplyViewport(rect)]; + instrs.append(&mut mesh_instrs); Some(DrawUpdate { key: self.dc_key,