Quellcode durchsuchen

wallet: migrate chatview to new PageManager construct

darkfi vor 1 Jahr
Ursprung
Commit
7bb858ef91
3 geänderte Dateien mit 307 neuen und 402 gelöschten Zeilen
  1. 79 369
      bin/darkwallet/src/ui/chatview/mod.rs
  2. 222 31
      bin/darkwallet/src/ui/chatview/page.rs
  3. 6 2
      bin/darkwallet/src/ui/mod.rs

+ 79 - 369
bin/darkwallet/src/ui/chatview/mod.rs

@@ -32,6 +32,7 @@ use std::{
 };
 
 mod page;
+use page::{Message as Message2, Page as Page2};
 
 use crate::{
     gfx::{
@@ -59,24 +60,6 @@ fn is_zero(x: f32) -> bool {
     x.abs() < EPSILON
 }
 
-// Replace vec item with N items
-fn replace_vec_item<T>(vec: &mut Vec<T>, idx: usize, mut items: Vec<T>) {
-    assert!(idx < vec.len());
-    if items.len() == 1 {
-        let item = items.remove(0);
-        let _ = std::mem::replace(&mut vec[idx], item);
-        return
-    }
-
-    let mut drain_iter = vec.drain(idx..);
-    // Drop the item at idx which will be replaced
-    drain_iter.next().unwrap();
-    let mut tail: Vec<_> = drain_iter.collect();
-
-    vec.append(&mut items);
-    vec.append(&mut tail);
-}
-
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct ChatMsg {
     pub nick: String,
@@ -86,153 +69,9 @@ pub struct ChatMsg {
 type Timestamp = u64;
 type MessageId = [u8; 32];
 
-#[derive(Clone)]
-struct Message {
-    timest: Timestamp,
-    #[allow(dead_code)]
-    id: MessageId,
-    chatmsg: ChatMsg,
-    glyphs: Vec<Glyph>,
-}
-
 const PAGE_SIZE: usize = 10;
 const PRELOAD_PAGES: usize = 10;
 
-#[derive(Clone)]
-struct PageMeshInfo {
-    px_height: f32,
-    mesh: DrawMesh,
-}
-
-type PagePtr = Arc<Page>;
-
-struct Page {
-    msgs: Vec<Message>,
-    atlas: SyncMutex<text::RenderedAtlas>,
-    // One draw call per page.
-    // Resizing the canvas means we recalc wrapping and the mesh changes
-    mesh_inf: SyncMutex<Option<PageMeshInfo>>,
-}
-
-impl Page {
-    async fn new(msgs: Vec<Message>, render_api: &RenderApi) -> Arc<Self> {
-        let mut atlas = text::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: SyncMutex::new(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,
-        debug_render: bool,
-    ) -> (PageMeshInfo, Option<DrawMesh>) {
-        let mut wrapped_line_idx = 0;
-
-        let mut mesh = MeshBuilder::new();
-
-        let atlas = self.atlas.lock().unwrap().clone();
-        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 = text::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 + 1) as f32 * line_height;
-
-                if i == last_idx {
-                    section = 0;
-                }
-
-                // debug draw baseline
-                if debug_render {
-                    let y = baseline - off_y;
-                    mesh.draw_filled_box(
-                        &Rectangle { x: 0., y: y - 1., w: clip.w, h: 1. },
-                        COLOR_BLUE,
-                    );
-                }
-
-                // Render line
-                let glyph_pos_iter = GlyphPositionIter::new(font_size, &line, baseline);
-                for (mut glyph_rect, glyph) in glyph_pos_iter.zip(line.iter()) {
-                    let uv_rect = 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,
-                    };
-
-                    //if debug_render {
-                    //    mesh.draw_outline(&glyph_rect, COLOR_BLUE, 2.);
-                    //}
-
-                    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;
-
-        if debug_render {
-            mesh.draw_outline(
-                &Rectangle { x: 0., y: 0., w: clip.w, h: -px_height },
-                COLOR_GREEN,
-                1.,
-            );
-        }
-
-        let mesh = mesh.alloc(render_api).await.unwrap();
-        let mesh = mesh.draw_with_texture(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
-}
-
 #[derive(Clone)]
 struct TouchInfo {
     start_scroll: f32,
@@ -258,8 +97,7 @@ pub struct ChatView {
     text_shaper: TextShaperPtr,
     tree: sled::Tree,
 
-    pages: AsyncMutex<Vec<PagePtr>>,
-    pages2: AsyncMutex<page::PageManager>,
+    pages: AsyncMutex<page::PageManager>,
     dc_key: u64,
 
     /// Used for detecting when scrolling view
@@ -372,11 +210,11 @@ impl ChatView {
             }
             on_modify.when_change(scroll.prop(), reload_view);
 
-            async fn redraw(self_: Arc<ChatView>) {
-                self_.redraw().await;
-            }
-            on_modify.when_change(rect.clone(), redraw);
-            on_modify.when_change(debug.prop(), redraw);
+            //async fn redraw(self_: Arc<ChatView>) {
+            //    self_.redraw().await;
+            //}
+            //on_modify.when_change(rect.clone(), redraw);
+            //on_modify.when_change(debug.prop(), redraw);
 
             let mut tasks = vec![
                 mouse_wheel_task,
@@ -396,8 +234,7 @@ impl ChatView {
                 text_shaper: text_shaper.clone(),
                 tree,
 
-                pages: AsyncMutex::new(Vec::new()),
-                pages2: AsyncMutex::new(page::PageManager::new(render_api, text_shaper)),
+                pages: AsyncMutex::new(page::PageManager::new(render_api, text_shaper)),
                 dc_key: OsRng.gen(),
 
                 mouse_pos: SyncMutex::new(Point::from([0., 0.])),
@@ -667,83 +504,12 @@ impl ChatView {
         }
 
         // Add message to page
-        self.pages2.lock().await
+        self.pages
+            .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();
-        let timestr = dt.format("%H:%M").to_string();
-
-        let text = format!("{} {} {}", timestr, chatmsg.nick, chatmsg.text);
-        let glyphs = self.text_shaper.shape(text, self.font_size.get()).await;
-
-        // Now add message to page
-
-        let mut pages = self.pages.lock().await;
-        let mut idx = None;
-        for (i, page) in pages.iter_mut().enumerate() {
-            let first_timest = page.msgs.last().unwrap().timest;
-            let last_timest = page.msgs.first().unwrap().timest;
-
-            //debug!(target: "ui::chatview", "page {i} [{first_timest}, {last_timest}]");
-            if first_timest <= timest && timest <= last_timest {
-                //debug!(target: "ui::chatview", "found page {i} [{first_timest}, {last_timest}]");
-                idx = Some(i);
-                break
-            }
-        }
-        let idx = match idx {
-            Some(idx) => idx,
-            None => {
-                //debug!(target: "ui::chatview", "no page found");
-                0
-            }
-        };
-
-        // Maybe we can write this code below better
-        if pages.is_empty() {
-            let msgs = vec![Message { timest, id: message_id, chatmsg, glyphs }];
-            let page = Page::new(msgs, &self.render_api).await;
-            pages.push(page);
-            drop(pages);
-
-            let scroll = self.scroll.get();
-            self.scrollview(scroll).await;
-            return;
-        }
-
-        let page = &mut pages[idx];
-        let mut msgs = page.msgs.clone();
-        msgs.push(Message { timest, id: message_id, chatmsg, glyphs });
-        msgs.sort_unstable_by_key(|msg| msg.timest);
-        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.timest, msg.chatmsg);
-            }
-            debug!(target: "ui::chatview", "===============================");
-
-            let new_page = Page::new(page_msgs, &self.render_api).await;
-            new_pages.push(new_page);
-        }
-
-        replace_vec_item(&mut pages, idx, new_pages);
-
-        drop(pages);
-
         // This will refresh the view, so we just use this
         let scroll = self.scroll.get();
         self.scrollview(scroll).await;
@@ -860,9 +626,8 @@ impl ChatView {
     /// Load extra pages
     async fn preload_pages(&self) -> usize {
         // Get last page
-        let last_page = self.pages.lock().await.last().unwrap().clone();
         // get the current earliest timestamp
-        let last_timest = last_page.msgs.last().unwrap().timest;
+        let last_timest = self.pages.lock().await.last_timestamp();
 
         // iterate from there
         let key = last_timest.to_be_bytes();
@@ -895,16 +660,25 @@ impl ChatView {
             let text = format!("{} {} {}", timestr, chatmsg.nick, chatmsg.text);
             let glyphs = self.text_shaper.shape(text, self.font_size.get()).await;
 
-            msgs.push(Message { timest, id: message_id, chatmsg, glyphs });
+            let msg = Message2::new(
+                self.font_size.get(),
+                timest,
+                message_id,
+                chatmsg.nick,
+                chatmsg.text,
+                &self.text_shaper,
+            )
+            .await;
+            msgs.push(msg);
 
             if msgs.len() >= PAGE_SIZE {
                 debug!(target: "ui::chatview", "added new page. page_len={pages_len}");
-                let msgs = std::mem::take(&mut msgs);
-                let page = Page::new(msgs, &self.render_api).await;
 
+                let msgs = std::mem::take(&mut msgs);
+                let page = Page2::new(msgs, &self.render_api).await;
                 self.pages.lock().await.push(page);
-                pages_len += 1;
 
+                pages_len += 1;
                 if pages_len >= n {
                     break
                 }
@@ -914,8 +688,8 @@ impl ChatView {
         // Any remaining messages added to a short page
         if !msgs.is_empty() {
             debug!(target: "ui::chatview", "added final page. page_len={pages_len}");
-            let page = Page::new(msgs, &self.render_api).await;
 
+            let page = Page2::new(msgs, &self.render_api).await;
             self.pages.lock().await.push(page);
             pages_len += 1;
         }
@@ -925,51 +699,18 @@ impl ChatView {
         pages_len
     }
 
-    async fn get_total_height(&self, rect: &Rectangle, pages: &Vec<PagePtr>) -> f32 {
+    async fn draw_cached(&self, rect: Rectangle, scroll: &mut f32) -> Vec<DrawInstruction> {
         let font_size = self.font_size.get();
         let line_height = self.line_height.get();
         let baseline = self.baseline.get();
         let debug_render = self.debug.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 mut current_height = self.descent();
-
-        for page in pages {
-            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(),
-                            debug_render,
-                        )
-                        .await;
-                    assert!(old_drawmesh.is_none());
-                    mesh_inf
-                }
-            };
-
-            current_height += mesh_inf.px_height;
-        }
-
-        current_height
-    }
-
-    async fn draw_cached(&self, rect: Rectangle, scroll: &mut f32) -> Vec<DrawInstruction> {
-        let mut instrs = vec![];
-
         // When scrolling it can go negative so clamp it here
         if *scroll < 0. {
             *scroll = 0.;
@@ -977,22 +718,20 @@ impl ChatView {
 
         // Make sure we have enough pages loaded.
         // If there's no more to load then adjust the scroll.
-        let mut pages = self.pages.lock().await.clone();
-        let mut total_height = self.get_total_height(&rect, &pages).await;
+        let mut pages = self.pages.lock().await;
+        let mut total_height = pages.calc_total_height(line_height, baseline);
         while total_height < *scroll + rect.h {
             debug!(target: "ui::chatview", "draw_cached() loading more pages");
 
             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;
+            total_height = pages.calc_total_height(line_height, baseline);
 
             // No more pages available to load
             if n_loaded_pages == 0 {
                 break
             }
-
-            pages = self.pages.lock().await.clone();
         }
 
         // If lines aren't enough to fill the available buffer then start from the top
@@ -1000,15 +739,22 @@ impl ChatView {
 
         Self::clamp_scroll(scroll, total_height, rect.h);
 
-        let mut current_height = 0.;
-        for page in pages {
-            if current_height > *scroll + rect.h {
-                break
-            }
-
-            let mesh_inf = page.mesh_inf.lock().unwrap().clone();
-            let mesh_inf = mesh_inf.expect("preload above should regen mesh");
+        let meshes = pages
+            .gen_meshes(
+                &rect,
+                *scroll,
+                font_size,
+                line_height,
+                baseline,
+                &nick_colors,
+                timest_color.clone(),
+                text_color.clone(),
+                debug_render,
+            )
+            .await;
 
+        let mut current_height = 0.;
+        for (height, mesh) in 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
@@ -1021,9 +767,10 @@ impl ChatView {
                 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.push(DrawInstruction::Draw(mesh));
+
+            current_height += height;
         }
 
         instrs
@@ -1035,20 +782,7 @@ impl ChatView {
         debug!(target: "ui::chatview", "scrollview()");
         let old_scroll = self.scroll.get();
 
-        let sg = self.sg.lock().await;
-        let node = sg.get_node(self.node_id).unwrap();
-
-        let Some(parent_rect) = get_parent_rect(&sg, node) else {
-            return 0.;
-        };
-
-        if let Err(err) = eval_rect(self.rect.clone(), &parent_rect) {
-            panic!("Node {:?} bad rect property: {}", node, err);
-        }
-
-        let Ok(rect) = read_rect(self.rect.clone()) else {
-            panic!("Node {:?} bad rect property", node);
-        };
+        let rect = read_rect(self.rect.clone()).expect("bad rect property");
 
         let mut mesh_instrs = self.draw_cached(rect.clone(), &mut scroll).await;
         let mut instrs = vec![DrawInstruction::ApplyViewport(rect)];
@@ -1086,51 +820,22 @@ impl ChatView {
         }
     }
 
-    async fn regen_mesh(&self, rect: Rectangle) -> (Vec<DrawInstruction>, Vec<DrawMesh>) {
+    async fn regen_mesh(&self, rect: Rectangle) -> Vec<DrawInstruction> {
         let font_size = self.font_size.get();
         let line_height = self.line_height.get();
         let baseline = self.baseline.get();
         let debug_render = self.debug.get();
 
         let mut instrs = vec![];
-        let mut old_drawmesh = vec![];
+        //let mut old_drawmesh = vec![];
 
         let timest_color = self.timestamp_color.get();
         let text_color = self.text_color.get();
         let nick_colors = self.read_nick_colors();
 
-        let pages = self.pages.lock().await.clone();
-
-        let mut mesh_infs = vec![];
-        // First pass is to measure the height and generate the meshes
-        let mut current_height = self.descent();
-        for page in pages {
-            // We should be able to count lines and perform wrapping without having to
-            // generate the mesh and alloc buffers.
-            // We need to separate both these ops.
-            let (mesh_inf, old) = page
-                .regen_mesh(
-                    &rect,
-                    &self.render_api,
-                    font_size,
-                    line_height,
-                    baseline,
-                    &nick_colors,
-                    timest_color.clone(),
-                    text_color.clone(),
-                    debug_render,
-                )
-                .await;
-
-            current_height += mesh_inf.px_height;
-
-            if let Some(old) = old {
-                old_drawmesh.push(old);
-            }
-            mesh_infs.push(mesh_inf);
-        }
-
-        let total_height = current_height;
+        let mut pages = self.pages.lock().await;
+        pages.adjust_line_width(rect.w);
+        let total_height = pages.calc_total_height(line_height, baseline);
 
         // If lines aren't enough to fill the available buffer then start from the top
         let start_pos = if total_height < rect.h { total_height } else { rect.h };
@@ -1140,12 +845,23 @@ impl ChatView {
             self.scroll.set(scroll);
         }
 
-        let mut current_height = 0.;
-        for mesh_inf in mesh_infs {
-            if current_height > scroll + rect.h {
-                break
-            }
+        pages.invalidate_caches();
+        let meshes = pages
+            .gen_meshes(
+                &rect,
+                scroll,
+                font_size,
+                line_height,
+                baseline,
+                &nick_colors,
+                timest_color.clone(),
+                text_color.clone(),
+                debug_render,
+            )
+            .await;
 
+        let mut current_height = 0.;
+        for (height, mesh) in 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
@@ -1159,12 +875,12 @@ impl ChatView {
 
             instrs.push(DrawInstruction::ApplyMatrix(model));
 
-            instrs.push(DrawInstruction::Draw(mesh_inf.mesh));
+            instrs.push(DrawInstruction::Draw(mesh));
 
-            current_height += mesh_inf.px_height;
+            current_height += height;
         }
 
-        (instrs, old_drawmesh)
+        instrs
     }
 
     fn read_nick_colors(&self) -> Vec<Color> {
@@ -1183,23 +899,16 @@ impl ChatView {
 
     pub async fn draw(&self, sg: &SceneGraph, parent_rect: &Rectangle) -> Option<DrawUpdate> {
         debug!(target: "ui::chatview", "ChatView::draw()");
-        // Only used for debug messages
-        let node = sg.get_node(self.node_id).unwrap();
 
-        if let Err(err) = eval_rect(self.rect.clone(), parent_rect) {
-            panic!("Node {:?} bad rect property: {}", node, err);
-        }
-
-        let Ok(rect) = read_rect(self.rect.clone()) else {
-            panic!("Node {:?} bad rect property", node);
-        };
+        let rect = eval_rect(self.rect.clone(), parent_rect).expect("bad rect property");
 
         let timer = std::time::Instant::now();
-        let (mut mesh_instrs, old_drawmesh) = self.regen_mesh(rect.clone()).await;
+        let mut mesh_instrs = self.regen_mesh(rect.clone()).await;
         debug!(target: "ui::chatview", "regen_mesh() took {:?}", timer.elapsed());
 
         let freed_textures = vec![];
         let mut freed_buffers = vec![];
+        /*
         for old_mesh in old_drawmesh {
             freed_buffers.push(old_mesh.vertex_buffer);
             freed_buffers.push(old_mesh.index_buffer);
@@ -1207,6 +916,7 @@ impl ChatView {
             //    freed_textures.push(texture_id);
             //}
         }
+        */
 
         debug!(target: "ui::chatview", "chatview rect = {:?}", rect);
 

+ 222 - 31
bin/darkwallet/src/ui/chatview/page.rs

@@ -44,8 +44,12 @@ const PRELOAD_PAGES: usize = 10;
 type Timestamp = u64;
 type MessageId = [u8; 32];
 
+fn is_whitespace(s: &str) -> bool {
+    s.chars().all(char::is_whitespace)
+}
+
 #[derive(Clone)]
-struct Message {
+pub(super) struct Message {
     font_size: f32,
 
     timestamp: Timestamp,
@@ -59,7 +63,7 @@ struct Message {
 }
 
 impl Message {
-    async fn new(
+    pub(super) async fn new(
         font_size: f32,
 
         timestamp: Timestamp,
@@ -98,31 +102,34 @@ impl Message {
     }
 }
 
-#[derive(Clone)]
-struct PageMeshInfo {
-    px_height: f32,
-    mesh: DrawMesh,
+enum PageVisibility {
+    Null,
+    Invisible,
+    EnterTop,
+    Visible,
+    ExitBottom,
 }
 
-struct Page {
+pub(super) struct Page {
     msgs: Vec<Message>,
     atlas: text::RenderedAtlas,
-    mesh: Option<PageMeshInfo>,
+    mesh_cache: Option<DrawMesh>,
+    visible: PageVisibility,
 }
 
 impl Page {
-    async fn new(msgs: Vec<Message>, render_api: &RenderApi) -> Self {
+    pub(super) async fn new(msgs: Vec<Message>, 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 }
+        Self { msgs, atlas, mesh_cache: None, visible: PageVisibility::Null }
     }
 
-    fn invalidate(&mut self) -> Option<DrawMesh> {
-        std::mem::replace(&mut self.mesh, None).map(|m| m.mesh)
+    fn clear_mesh(&mut self) -> Option<DrawMesh> {
+        std::mem::replace(&mut self.mesh_cache, None)
     }
 
     async fn split(mut msgs: Vec<Message>, render_api: &RenderApi) -> Vec<Self> {
@@ -152,6 +159,127 @@ impl Page {
         assert!(new_pages.len() <= 2);
         new_pages
     }
+
+    fn wrapped_lines_count(&self) -> usize {
+        let mut count = 0;
+        for msg in &self.msgs {
+            count += msg.wrapped_lines.len();
+        }
+        count
+    }
+
+    async fn gen_mesh(
+        &mut self,
+        render_api: &RenderApi,
+        clip: &Rectangle,
+        font_size: f32,
+        line_height: f32,
+        baseline: f32,
+        nick_colors: &[Color],
+        timestamp_color: Color,
+        text_color: Color,
+        debug_render: bool,
+    ) -> DrawMesh {
+        if let Some(mesh) = &self.mesh_cache {
+            return mesh.clone()
+        }
+
+        let mut line_idx = 0;
+        let mut mesh = MeshBuilder::new();
+
+        for msg in &self.msgs {
+            let nick_color = select_nick_color(&msg.nick, nick_colors);
+
+            let last_idx = msg.wrapped_lines.len() - 1;
+            for (i, line) in msg.wrapped_lines.iter().rev().enumerate() {
+                let off_y = (line_idx + 1) as f32 * line_height;
+                let is_last_line = i == last_idx;
+
+                // debug draw baseline
+                if debug_render {
+                    let y = baseline - off_y;
+                    mesh.draw_filled_box(
+                        &Rectangle { x: 0., y: y - 1., w: clip.w, h: 1. },
+                        COLOR_BLUE,
+                    );
+                }
+
+                self.render_line(
+                    &mut mesh,
+                    line,
+                    off_y,
+                    is_last_line,
+                    font_size,
+                    baseline,
+                    timestamp_color,
+                    nick_color,
+                    text_color,
+                    debug_render,
+                );
+
+                line_idx += 1;
+            }
+        }
+
+        let mesh = mesh.alloc(render_api).await.unwrap();
+        let mesh = mesh.draw_with_texture(self.atlas.texture_id);
+        self.mesh_cache = Some(mesh.clone());
+
+        mesh
+    }
+
+    fn render_line(
+        &self,
+        mesh: &mut MeshBuilder,
+        line: &Vec<Glyph>,
+        off_y: f32,
+        is_last: bool,
+        font_size: f32,
+        baseline: f32,
+        timestamp_color: Color,
+        nick_color: Color,
+        text_color: Color,
+        debug_render: bool,
+    ) {
+        // 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;
+        if is_last {
+            section = 0;
+        }
+
+        let 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,
+            };
+
+            //if debug_render {
+            //    mesh.draw_outline(&glyph_rect, COLOR_BLUE, 2.);
+            //}
+
+            mesh.draw_box(&glyph_rect, color, uv_rect);
+
+            if is_last && section < 2 && is_whitespace(&glyph.substr) {
+                section += 1;
+            }
+        }
+    }
+}
+
+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 FreedData {
@@ -177,15 +305,22 @@ pub struct PageManager {
 }
 
 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 }
+    pub(super) fn new(render_api: RenderApiPtr, text_shaper: TextShaperPtr) -> Self {
+        Self {
+            pages: vec![],
+            freed: FreedData { buffers: vec![], textures: vec![] },
+            render_api,
+            text_shaper,
+        }
+    }
+
+    pub(super) fn push(&mut self, page: Page) {
+        self.pages.push(page);
     }
 
     /// 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) {
+    pub(super) 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);
@@ -193,20 +328,27 @@ impl PageManager {
         }
     }
 
-    fn calc_total_height(&self) -> f32 {
-        0.
+    pub(super) fn calc_total_height(&self, line_height: f32, baseline: f32) -> f32 {
+        let descent = line_height - baseline;
+        let mut height = descent;
+
+        for page in &self.pages {
+            let lines_count = page.wrapped_lines_count();
+            height += lines_count as f32 * line_height;
+        }
+
+        height
     }
 
     pub(super) async fn insert_line(
         &mut self,
         font_size: f32,
-        timestamp: Timestamp,
+        timest: Timestamp,
         message_id: MessageId,
         nick: String,
         text: String,
     ) {
-        let msg =
-            Message::new(font_size, timestamp, message_id, nick, text, &self.text_shaper).await;
+        let msg = Message::new(font_size, timest, message_id, nick, text, &self.text_shaper).await;
 
         // Now add message to page
 
@@ -219,11 +361,19 @@ impl PageManager {
 
         let mut idx = None;
         for (i, page) in self.pages.iter_mut().enumerate() {
-            //let first_timest = page.msgs.last().unwrap().timest;
+            let first_timest = page.msgs.last().unwrap().timestamp;
             let last_timest = page.msgs.first().unwrap().timestamp;
 
+            if timest < first_timest {
+                // It does not belong to any current page
+                // Create page only if there's not enough pages for the screen rect
+                // current_height < rect.h
+                // Otherwise it just means the page wasn't loaded
+                // Maybe we need to have a flag indicating there's pages not loaded in the DB still
+            }
+
             //debug!(target: "ui::chatview", "page {i} [{first_timest}, {last_timest}]");
-            if timestamp <= last_timest {
+            if first_timest <= timest && timest <= last_timest {
                 //debug!(target: "ui::chatview", "found page {i} [{first_timest}, {last_timest}]");
                 idx = Some(i);
                 break
@@ -247,6 +397,11 @@ impl PageManager {
         let mut tail: Vec<_> = drain_iter.collect();
         assert_eq!(old_pages_len, head.len() + 1 + tail.len());
 
+        // Free texture and mesh before dropping page
+        if let Some(mesh) = old_page.mesh_cache {
+            self.freed.add(mesh);
+        }
+
         let mut msgs = old_page.msgs;
         msgs.push(msg);
         let mut new_pages = Page::split(msgs, &self.render_api).await;
@@ -257,19 +412,19 @@ impl PageManager {
     }
 
     /// Clear all meshes and caches. Returns data that needs to be freed.
-    fn invalidate_caches(&mut self) {
+    pub(super) fn invalidate_caches(&mut self) {
         for page in &mut self.pages {
-            if let Some(mesh) = page.invalidate() {
+            if let Some(mesh) = page.clear_mesh() {
                 self.freed.add(mesh);
             }
         }
     }
 
     /// Generate caches and return meshes
-    pub(super) async fn get_meshes(
-        &self,
-        clip: &Rectangle,
-        render_api: &RenderApi,
+    pub(super) async fn gen_meshes(
+        &mut self,
+        rect: &Rectangle,
+        scroll: f32,
         font_size: f32,
         line_height: f32,
         baseline: f32,
@@ -277,6 +432,42 @@ impl PageManager {
         timestamp_color: Color,
         text_color: Color,
         debug_render: bool,
-    ) {
+    ) -> Vec<(f32, DrawMesh)> {
+        let mut meshes = vec![];
+
+        let descent = line_height - baseline;
+        let mut current_height = descent;
+        for page in &mut self.pages {
+            if current_height > scroll + rect.h {
+                break
+            }
+
+            let mesh = page
+                .gen_mesh(
+                    &self.render_api,
+                    rect,
+                    font_size,
+                    line_height,
+                    baseline,
+                    nick_colors,
+                    timestamp_color,
+                    text_color,
+                    debug_render,
+                )
+                .await;
+
+            let lines_count = page.wrapped_lines_count();
+            let mesh_height = lines_count as f32 * line_height;
+
+            meshes.push((mesh_height, mesh));
+
+            current_height += mesh_height;
+        }
+
+        meshes
+    }
+
+    pub(super) fn last_timestamp(&self) -> Timestamp {
+        self.pages.last().unwrap().msgs.last().unwrap().timestamp
     }
 }

+ 6 - 2
bin/darkwallet/src/ui/mod.rs

@@ -107,11 +107,13 @@ impl<T: Send + Sync + 'static> OnModify<T> {
     }
 }
 
-pub fn eval_rect(rect: PropertyPtr, parent_rect: &Rectangle) -> Result<()> {
+pub fn eval_rect(rect: PropertyPtr, parent_rect: &Rectangle) -> Result<Rectangle> {
     if rect.array_len != 4 {
         return Err(Error::PropertyWrongLen)
     }
 
+    let mut rect_arr = [0.; 4];
+
     for i in 0..4 {
         if !rect.is_expr(i)? {
             continue
@@ -129,8 +131,10 @@ pub fn eval_rect(rect: PropertyPtr, parent_rect: &Rectangle) -> Result<()> {
 
         let v = machine.call()?.as_f32()?;
         rect.set_cache_f32(i, v).unwrap();
+
+        rect_arr[i] = v;
     }
-    Ok(())
+    Ok(Rectangle::from_array(rect_arr))
 }
 
 pub fn read_rect(rect_prop: PropertyPtr) -> Result<Rectangle> {