فهرست منبع

wallet: chatview rendering text

darkfi 2 سال پیش
والد
کامیت
17d837ee6c

+ 1 - 1
bin/darkwallet/src/app.rs

@@ -464,7 +464,7 @@ fn populate_tree(tree: &sled::Tree) {
         let key = timest.to_be_bytes();
         //timest.encode(&mut key).unwrap();
 
-        let line = chatview::ChatLine { nick, text };
+        let msg = chatview::ChatMsg { nick, text };
         let mut val = vec![];
         line.encode(&mut val).unwrap();
 

+ 1 - 0
bin/darkwallet/src/text2/atlas.rs

@@ -173,6 +173,7 @@ fn copy_image(sprite: &Sprite, x: usize, y: usize, atlas: &mut Vec<u8>, atlas_wi
 }
 
 /// Final result computed from `Atlas::make()`.
+#[derive(Clone)]
 pub struct RenderedAtlas {
     glyph_ids: Vec<u32>,
     /// UV rectangle within the texture.

+ 4 - 1
bin/darkwallet/src/text2/mod.rs

@@ -39,11 +39,14 @@ use crate::{
 };
 
 mod atlas;
-pub use atlas::{make_texture_atlas, Atlas};
+pub use atlas::{make_texture_atlas, Atlas, RenderedAtlas};
 
 //mod old_atlas;
 //pub use old_atlas::{make_texture_atlas, RenderedAtlas};
 
+mod wrap;
+pub use wrap::wrap;
+
 // From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
 //
 // * An `FT_Face' object can only be safely used from one thread at

+ 150 - 0
bin/darkwallet/src/text2/wrap.rs

@@ -0,0 +1,150 @@
+use super::{Glyph, GlyphPositionIter};
+
+#[derive(Debug, PartialEq)]
+#[repr(u8)]
+enum TokenType {
+    Null,
+    Word,
+    Whitespace,
+}
+
+struct Token {
+    token_type: TokenType,
+    lhs: f32,
+    rhs: f32,
+    glyphs: Vec<Glyph>,
+}
+
+impl Token {
+    fn as_str(&self) -> String {
+        glyph_str(&self.glyphs)
+    }
+}
+
+/// Get the string represented by a vec of glyphs. Useful for debugging.
+fn glyph_str(glyphs: &Vec<Glyph>) -> String {
+    glyphs.iter().map(|g| g.substr.as_str()).collect::<Vec<_>>().join("")
+}
+
+fn tokenize(font_size: f32, glyphs: &Vec<Glyph>) -> Vec<Token> {
+    let glyph_pos_iter = GlyphPositionIter::new(font_size, glyphs, 0.);
+
+    let mut tokens = vec![];
+    let mut token_glyphs = vec![];
+    let mut lhs = -1.;
+    let mut rhs = 0.;
+
+    let mut token_type = TokenType::Null;
+
+    for (pos, glyph) in glyph_pos_iter.zip(glyphs.iter()) {
+        let new_type = if glyph.substr.chars().all(char::is_whitespace) {
+            TokenType::Whitespace
+        } else {
+            TokenType::Word
+        };
+
+        // This is the initial token so lets begin
+        // Just assume the token_type
+        if token_type == TokenType::Null {
+            assert!(token_glyphs.is_empty());
+            token_type = new_type;
+        } else if new_type != token_type {
+            // We just changed from one token type to another
+            assert!(!token_glyphs.is_empty());
+
+            // We have a non-empty word to push
+            let token = Token { token_type, lhs, rhs, glyphs: std::mem::take(&mut token_glyphs) };
+            tokens.push(token);
+
+            // Reset ruler
+            lhs = -1.;
+            rhs = 0.;
+            // take() blanked token_glyphs above
+
+            token_type = new_type;
+        }
+
+        // LHS is uninitialized so this is the first glyph in the word
+        if lhs < 0. {
+            lhs = pos.x;
+        }
+
+        // RHS should always be the max
+        rhs = pos.x + pos.w;
+
+        // Update word
+        token_glyphs.push(glyph.clone());
+    }
+
+    if !token_glyphs.is_empty() {
+        let token = Token { token_type, lhs, rhs, glyphs: std::mem::take(&mut token_glyphs) };
+        tokens.push(token);
+    }
+
+    tokens
+}
+
+/// Given a series of words, apply wrapping.
+/// Whitespace is perserved unless the word wraps.
+fn apply_wrap(line_width: f32, tokens: Vec<Token>) -> Vec<Vec<Glyph>> {
+    let mut lines = vec![];
+    let mut line = vec![];
+    let mut start = 0.;
+
+    for (i, mut token) in tokens.into_iter().enumerate() {
+        assert!(token.token_type != TokenType::Null);
+
+        // Triggered by if below
+        if start < 0. {
+            assert_eq!(token.token_type, TokenType::Word);
+            start = token.lhs;
+        }
+
+        // Does this token cross over the end of the line?
+        if token.rhs > start + line_width {
+            // Start a new line
+            let line = std::mem::take(&mut line);
+            //debug!(target: "text::apply_wrap", "adding line: {}", glyph_str(&line));
+            lines.push(line);
+
+            // Whitespace tokens that cause wrapping are just discarded.
+            if token.token_type == TokenType::Whitespace {
+                // Load LHS from next token in loop
+                start = -1.;
+                continue
+            }
+
+            assert_eq!(token.token_type, TokenType::Word);
+            start = token.lhs;
+        }
+
+        line.append(&mut token.glyphs);
+    }
+
+    // Handle the remainders
+    if !line.is_empty() {
+        let line = std::mem::take(&mut line);
+        //debug!(target: "text::apply_wrap", "adding line: {}", glyph_str(&line));
+        lines.push(line);
+    }
+
+    lines
+}
+
+pub fn wrap(line_width: f32, font_size: f32, glyphs: &Vec<Glyph>) -> Vec<Vec<Glyph>> {
+    let tokens = tokenize(font_size, glyphs);
+
+    //debug!(target: "text::wrap", "tokenized words {:?}",
+    //       words.iter().map(|w| w.as_str()).collect::<Vec<_>>());
+
+    let lines = apply_wrap(line_width, tokens);
+
+    //if lines.len() > 1 {
+    //    debug!(target: "text::wrap", "wrapped line: {}", glyph_str(glyphs));
+    //    for line in &lines {
+    //        debug!(target: "text::wrap", "-> {}", glyph_str(line));
+    //    }
+    //}
+
+    lines
+}

+ 95 - 24
bin/darkwallet/src/ui/chatview.rs

@@ -41,20 +41,30 @@ use crate::{
 
 use super::{eval_rect, get_parent_rect, read_rect, DrawUpdate, OnModify, Stoppable};
 
-#[derive(Debug, SerialEncodable, SerialDecodable)]
-pub struct ChatLine {
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct ChatMsg {
     pub nick: String,
     pub text: String,
 }
 
 type Timestamp = u32;
 
-struct RenderLine {
+#[derive(Clone)]
+struct Message {
     timest: Timestamp,
-    chatline: ChatLine,
+    chatmsg: ChatMsg,
     glyphs: Vec<Glyph>,
 }
 
+const LINES_PER_PAGE: usize = 10;
+const PRELOAD_PAGES: usize = 200;
+
+#[derive(Clone)]
+struct Page {
+    msgs: Vec<Message>,
+    atlas: text2::RenderedAtlas,
+}
+
 pub type ChatViewPtr = Arc<ChatView>;
 
 pub struct ChatView {
@@ -63,8 +73,7 @@ pub struct ChatView {
     text_shaper: TextShaperPtr,
     tree: sled::Tree,
 
-    lines: SyncMutex<Vec<RenderLine>>,
-    drawcalls: SyncMutex<Option<Vec<DrawInstruction>>>,
+    pages: SyncMutex<Vec<Page>>,
     dc_key: u64,
 
     rect: PropertyPtr,
@@ -99,8 +108,7 @@ impl ChatView {
             text_shaper,
             tree,
 
-            lines: SyncMutex::new(Vec::new()),
-            drawcalls: SyncMutex::new(None),
+            pages: SyncMutex::new(Vec::new()),
             dc_key: OsRng.gen(),
 
             rect,
@@ -116,33 +124,101 @@ impl ChatView {
     }
 
     async fn populate(&self) {
-        let mut lines = vec![];
+        let mut pages = vec![];
+        let mut msgs = vec![];
 
         for entry in self.tree.iter().rev() {
             let Ok((k, v)) = entry else { break };
             assert_eq!(k.len(), 4);
             let key_bytes: [u8; 4] = k.as_ref().try_into().unwrap();
             let timest = Timestamp::from_be_bytes(key_bytes);
-            let chatline: ChatLine = deserialize(&v).unwrap();
-            //println!("{k:?} {chatline:?}");
+            let chatmsg: ChatMsg = deserialize(&v).unwrap();
+            //println!("{k:?} {chatmsg:?}");
 
             let timestr = timest.to_string();
             // left pad with zeros
             let mut timestr = format!("{:0>4}", timestr);
             timestr.insert(2, ':');
 
-            let text = format!("{} {} {}", timestr, chatline.nick, chatline.text);
+            let text = format!("{} {} {}", timestr, chatmsg.nick, chatmsg.text);
             let glyphs = self.text_shaper.shape(text, self.font_size.get()).await;
 
-            lines.push(RenderLine { timest, chatline, glyphs });
+            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 = Page { msgs: std::mem::take(&mut msgs), atlas };
+                pages.push(page);
+
+                if pages.len() >= PRELOAD_PAGES {
+                    break
+                }
+            }
         }
-        *self.lines.lock().unwrap() = lines;
+        debug!(target: "ui::chatview", "populated {} pages", pages.len());
+        *self.pages.lock().unwrap() = pages;
     }
 
     async fn regen_mesh(&self, mut clip: Rectangle) -> Vec<DrawInstruction> {
+        let font_size = self.font_size.get();
+        let line_height = self.line_height.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.
-        vec![]
+        let pages = self.pages.lock().unwrap().clone();
+
+        let mut draws = vec![];
+        let color = COLOR_WHITE;
+
+        // Pages start at the bottom.
+        let mut height = 0;
+        'pageloop: for page in pages {
+            let mut mesh = MeshBuilder::new();
+
+            for msg in page.msgs {
+                let glyphs = msg.glyphs;
+
+                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();
+                for line in lines {
+                    let px_height = height as f32 * line_height;
+
+                    if px_height > clip.h {
+                        break 'pageloop;
+                    }
+
+                    // Render line
+                    let mut glyph_pos_iter =
+                        GlyphPositionIter::new(font_size, &line, clip.h - px_height);
+                    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");
+                        mesh.draw_box(&glyph_rect, color, uv_rect);
+                    }
+
+                    height += 1;
+                }
+            }
+
+            let mesh = mesh.alloc(&self.render_api).await.unwrap();
+
+            draws.push(DrawInstruction::Draw(DrawMesh {
+                vertex_buffer: mesh.vertex_buffer,
+                index_buffer: mesh.index_buffer,
+                texture: Some(page.atlas.texture_id),
+                num_elements: mesh.num_elements,
+            }));
+        }
+
+        draws
     }
 
     pub async fn draw(&self, sg: &SceneGraph, parent_rect: &Rectangle) -> Option<DrawUpdate> {
@@ -161,16 +237,11 @@ impl ChatView {
         rect.x += parent_rect.x;
         rect.y += parent_rect.y;
 
-        let drawcalls = self.drawcalls.lock().unwrap().clone();
-        let mut drawcalls = match drawcalls {
-            Some(drawcalls) => drawcalls,
-            None => {
-                let drawcalls = self.regen_mesh(rect.clone()).await;
-                *self.drawcalls.lock().unwrap() = Some(drawcalls.clone());
-                drawcalls
-            }
-        };
+        let mut drawcalls = self.regen_mesh(rect.clone()).await;
+        // TODO: delete old buffers
 
+        // Apply scroll and scissor
+        // We use the scissor for scrolling
         let off_x = rect.x / parent_rect.w;
         let off_y = rect.y / parent_rect.h;
         let scale_x = 1. / parent_rect.w;