فهرست منبع

wallet: vastly improve the text atlassing util

darkfi 2 سال پیش
والد
کامیت
58b67fe388

+ 1 - 0
bin/darkwallet/Cargo.toml

@@ -35,6 +35,7 @@ futures = "0.3.30"
 async-recursion = "1.1.1"
 colored = "2.1.0"
 #rustpython-vm = "0.3.1"
+sled = "0.34.7"
 
 [patch.crates-io]
 freetype-rs = { git = "https://github.com/narodnik/freetype-rs" }

+ 0 - 20
bin/darkwallet/chat.txt

@@ -1,23 +1,3 @@
-21:21 somiaj Z-module: am I messing up the logic now...grr
-21:22 somiaj rex: If a is a free variable, then you need a universe, though I guess if you already know something about a you don't, thanks for pointing that out.
-21:22 rex no I mean things like -1 \not\in ℕ
-21:22 rex why do I need a universe here?
-21:23 somiaj yea, that is what I was stating, if you knew something about the object, it wasn't a free variable, then you are correct, a universal set isn't needed
-21:23 rex that's what you meant. I see
-21:23 somiaj I was (maybe incorrectly) assuming that x and y were free variables here (vs already in some other set or something is known about them, like yoru example)
-21:24 somiaj so yea, I guess there are contexts in which you can use not in A, but A^c may not be welldefined
-21:28 rex with free variables you mean, that it is implicit to what universe they belong?
-21:43 mh_le Z-module: can I get you to take a look at a solution?
-21:57 mahboubine I've got a function and I calculated it's derivative to determine whether the function is increasing or decreasing at different intervals.
-21:59 mahboubine but the exercise correction also found the points from which the curve stars descending or ascending, for instance f(x) decends from 0 to -1/4 and back up until 0 and so on
-21:59 mahboubine I am wondering how to find those points as well
-22:01 blackfield well you're looking for points c such that f'(c)=0, or undefined..
-22:04 blackfield then you can test f''(c)>0 (then the point is a local minimum), or if f''(c)<0 it's a local maximum
-22:08 mahboubine I came to that conclusion as well
-22:08 mahboubine pretty easy now that I see it.
-22:08 mahboubine thanks, blackfield
-22:08 blackfield :)
-23:51 biberao hi
 00:53 bouma would you call within a 75% CI weak evidence ? i would call it no evidence
 01:00 somiaj bouma: I would just call it a 75% CI, so 1 and 4 chance you are wrong.
 01:00 somiaj I don't think a p value of 0.25 is that common, most prefer 0.05 or smaller

+ 49 - 4
bin/darkwallet/src/app.rs

@@ -17,6 +17,7 @@
  */
 
 use async_recursion::async_recursion;
+use darkfi_serial::Encodable;
 use futures::{stream::FuturesUnordered, StreamExt};
 use std::{sync::Arc, thread};
 
@@ -26,7 +27,7 @@ use crate::{
     prop::{Property, PropertySubType, PropertyType},
     scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId, SceneNodeType},
     text2::TextShaperPtr,
-    ui::{ChatView, EditBox, Mesh, RenderLayer, Stoppable, Text, Window},
+    ui::{chatview, ChatView, EditBox, Mesh, RenderLayer, Stoppable, Text, Window},
 };
 
 //fn print_type_of<T>(_: &T) {
@@ -401,18 +402,30 @@ impl App {
         let node = sg.get_node(node_id).unwrap();
         let prop = node.get_property("rect").unwrap();
         prop.set_f32(0, 0.).unwrap();
-        prop.set_f32(1, 0.).unwrap();
+        prop.set_f32(1, 200.).unwrap();
         let code = vec![Op::LoadVar("w".to_string())];
         prop.set_expr(2, code).unwrap();
         let code = vec![Op::Sub((
             Box::new(Op::LoadVar("h".to_string())),
-            Box::new(Op::ConstFloat32(50.)),
+            Box::new(Op::ConstFloat32(200.)),
         ))];
         prop.set_expr(3, code).unwrap();
+        node.set_property_f32("font_size", 20.).unwrap();
+        node.set_property_f32("line_height", 30.).unwrap();
         node.set_property_u32("z_index", 1).unwrap();
 
         drop(sg);
-        let pimpl = ChatView::new().await;
+        let db = sled::open("chatdb").expect("cannot open sleddb");
+        let chat_tree = db.open_tree(b"chat").unwrap();
+        //populate_tree(&chat_tree);
+        let pimpl = ChatView::new(
+            self.sg.clone(),
+            node_id,
+            self.render_api.clone(),
+            self.text_shaper.clone(),
+            chat_tree,
+        )
+        .await;
         let mut sg = self.sg.lock().await;
         let node = sg.get_node_mut(node_id).unwrap();
         node.pimpl = pimpl;
@@ -437,6 +450,28 @@ impl App {
     }
 }
 
+// Just for testing
+fn populate_tree(tree: &sled::Tree) {
+    let chat_txt = include_str!("../chat.txt");
+    for line in chat_txt.lines() {
+        let parts: Vec<&str> = line.splitn(3, ' ').collect();
+        assert_eq!(parts.len(), 3);
+        let timest = parts[0].replace(':', "").parse::<u32>().unwrap();
+        let nick = parts[1].to_string();
+        let text = parts[2].to_string();
+
+        // serial order is important here
+        let key = timest.to_be_bytes();
+        //timest.encode(&mut key).unwrap();
+
+        let line = chatview::ChatLine { nick, text };
+        let mut val = vec![];
+        line.encode(&mut val).unwrap();
+
+        tree.insert(&key, val).unwrap();
+    }
+}
+
 pub fn create_layer(sg: &mut SceneGraph, name: &str) -> SceneNodeId {
     let node = sg.add_node(name, SceneNodeType::RenderLayer);
     let prop = Property::new("is_visible", PropertyType::Bool, PropertySubType::Null);
@@ -563,6 +598,16 @@ fn create_chatview(sg: &mut SceneGraph, name: &str) -> SceneNodeId {
     prop.allow_exprs();
     node.add_property(prop).unwrap();
 
+    let mut prop = Property::new("scroll", PropertyType::Float32, PropertySubType::Null);
+    prop.set_ui_text("Scroll", "Scroll up from the bottom");
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("font_size", PropertyType::Float32, PropertySubType::Pixel);
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("line_height", PropertyType::Float32, PropertySubType::Pixel);
+    node.add_property(prop).unwrap();
+
     let mut prop = Property::new("z_index", PropertyType::Uint32, PropertySubType::Null);
     node.add_property(prop).unwrap();
 

+ 3 - 0
bin/darkwallet/src/error.rs

@@ -122,4 +122,7 @@ pub enum Error {
 
     #[error("Publisher was destroyed")]
     PublisherDestroyed = 34,
+
+    #[error("Empty atlas")]
+    AtlasIsEmpty = 35,
 }

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

@@ -251,7 +251,7 @@ pub struct DrawMesh {
     pub num_elements: i32,
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub enum DrawInstruction {
     ApplyViewport(Rectangle),
     ApplyMatrix(glam::Mat4),

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

@@ -0,0 +1,197 @@
+use miniquad::TextureId;
+
+use crate::{
+    error::{Error, Result},
+    gfx2::{Rectangle, RenderApi, RenderApiPtr},
+    util::{ansi_texture, zip3},
+};
+
+use super::{Glyph, Sprite, SpritePtr};
+
+/// Prevents render artifacts from aliasing.
+/// Even with aliasing turned off, some bleed still appears possibly
+/// due to UV coord calcs. Adding a gap perfectly fixes this.
+const ATLAS_GAP: usize = 2;
+
+/// Convenience wrapper fn. Use if rendering a single line of glyphs.
+pub async fn make_texture_atlas(
+    render_api: &RenderApi,
+    glyphs: &Vec<Glyph>,
+) -> Result<RenderedAtlas> {
+    let mut atlas = Atlas::new(render_api);
+    atlas.push(&glyphs);
+    atlas.make().await
+}
+
+/// Responsible for aggregating glyphs, and then producing a single software
+/// blitted texture usable in a single draw call.
+/// This makes OpenGL batch precomputation of meshes efficient.
+///
+/// ```rust
+///     let mut atlas = Atlas::new(&render_api);
+///     atlas.push(&glyphs);    // repeat as needed for shaped lines
+///     let atlas = atlas.make().unwrap();
+///     let uv = atlas.fetch_uv(glyph_id).unwrap();
+///     let atlas_texture_id = atlas.texture_id;
+/// ```
+pub struct Atlas<'a> {
+    glyph_ids: Vec<u32>,
+    sprites: Vec<SpritePtr>,
+    // LHS x pos of glyph
+    x_pos: Vec<usize>,
+
+    width: usize,
+    height: usize,
+
+    render_api: &'a RenderApi,
+}
+
+impl<'a> Atlas<'a> {
+    pub fn new(render_api: &'a RenderApi) -> Self {
+        Self {
+            glyph_ids: vec![],
+            sprites: vec![],
+            x_pos: vec![],
+            width: ATLAS_GAP,
+            // Not really important to set a value here since it will
+            // get overwritten.
+            // FYI glyphs have a gap on all sides (top and bottom here).
+            height: 2 * ATLAS_GAP,
+            render_api,
+        }
+    }
+
+    fn push_glyph(&mut self, glyph: &Glyph) {
+        if self.glyph_ids.contains(&glyph.glyph_id) {
+            return
+        }
+
+        self.glyph_ids.push(glyph.glyph_id);
+        self.sprites.push(glyph.sprite.clone());
+
+        let sprite = &glyph.sprite;
+        self.x_pos.push(self.width);
+
+        // Gap on the top and bottom
+        let height = ATLAS_GAP + sprite.bmp_height + ATLAS_GAP;
+        self.height = std::cmp::max(height, self.height);
+
+        // Gap between glyphs and on both sides
+        self.width += sprite.bmp_width + ATLAS_GAP;
+    }
+
+    /// Push a line of shaped text represented as `Vec<Glyph>`
+    /// to this atlas.
+    pub fn push(&mut self, glyphs: &Vec<Glyph>) {
+        for glyph in glyphs {
+            self.push_glyph(glyph);
+        }
+    }
+
+    fn render(&self) -> Vec<u8> {
+        let mut atlas = vec![0; 4 * self.width * self.height];
+        // For drawing debug lines we want a single white pixel.
+        // This is very useful to have in our texture for debugging.
+        atlas[0] = 255;
+        atlas[1] = 255;
+        atlas[2] = 255;
+        atlas[3] = 255;
+
+        let y = ATLAS_GAP;
+        // Copy all the sprites to our atlas.
+        // They should have ATLAS_GAP spacing on all sides to avoid bleeding.
+        for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) {
+            copy_image(sprite, *x, y, &mut atlas, self.width);
+        }
+
+        atlas
+    }
+
+    fn compute_uvs(&self) -> Vec<Rectangle> {
+        // UV coords are in the range [0, 1]
+        let mut uvs = vec![];
+
+        let (self_w, self_h) = (self.width as f32, self.height as f32);
+        let y = ATLAS_GAP as f32;
+
+        for (sprite, x) in self.sprites.iter().zip(self.x_pos.iter()) {
+            let x = *x as f32;
+            let sprite_w = sprite.bmp_width as f32;
+            let sprite_h = sprite.bmp_height as f32;
+
+            let uv = Rectangle {
+                x: x / self_w,
+                y: y / self_h,
+                w: sprite_w / self_w,
+                h: sprite_h / self_h,
+            };
+            uvs.push(uv);
+        }
+
+        uvs
+    }
+
+    /// Invalidate this atlas and produce the finalized result.
+    /// Each glyph is given a sub-rect within the texture, accessible by calling
+    /// `rendered_atlas.fetch_uv(my_glyph_id)`.
+    /// The texture ID is a struct member: `rendered_atlas.texture_id`.
+    pub async fn make(self) -> Result<RenderedAtlas> {
+        if self.glyph_ids.is_empty() {
+            return Err(Error::AtlasIsEmpty);
+        }
+        assert_eq!(self.glyph_ids.len(), self.sprites.len());
+        assert_eq!(self.glyph_ids.len(), self.x_pos.len());
+
+        let atlas = self.render();
+        let texture_id =
+            self.render_api.new_texture(self.width as u16, self.height as u16, atlas).await?;
+
+        let uv_rects = self.compute_uvs();
+        let glyph_ids = self.glyph_ids;
+
+        Ok(RenderedAtlas { glyph_ids, uv_rects, texture_id })
+    }
+}
+
+/// Copy a sprite to (x, y) position within the atlas texture.
+/// Both image formats are RGBA flat vecs.
+fn copy_image(sprite: &Sprite, x: usize, y: usize, atlas: &mut Vec<u8>, atlas_width: usize) {
+    for i in 0..sprite.bmp_height {
+        for j in 0..sprite.bmp_width {
+            let src_y = i * sprite.bmp_width;
+            let off_src = 4 * (src_y + j);
+
+            let dest_y = (y + i) * atlas_width;
+            let off_dest = 4 * (dest_y + j + x);
+
+            atlas[off_dest] = sprite.bmp[off_src];
+            atlas[off_dest + 1] = sprite.bmp[off_src + 1];
+            atlas[off_dest + 2] = sprite.bmp[off_src + 2];
+            atlas[off_dest + 3] = sprite.bmp[off_src + 3];
+        }
+    }
+}
+
+/// Final result computed from `Atlas::make()`.
+pub struct RenderedAtlas {
+    glyph_ids: Vec<u32>,
+    /// UV rectangle within the texture.
+    uv_rects: Vec<Rectangle>,
+    /// Allocated atlas texture. Must be manually deallocated by the user.
+    pub texture_id: TextureId,
+}
+
+impl RenderedAtlas {
+    /// Get UV coords for a glyph within the rendered atlas.
+    pub fn fetch_uv(&self, glyph_id: u32) -> Option<&Rectangle> {
+        let glyphs_len = self.glyph_ids.len();
+        assert_eq!(glyphs_len, self.uv_rects.len());
+
+        for i in 0..glyphs_len {
+            if self.glyph_ids[i] == glyph_id {
+                return Some(&self.uv_rects[i])
+            }
+        }
+        None
+    }
+}

+ 8 - 106
bin/darkwallet/src/text2.rs → bin/darkwallet/src/text2/mod.rs

@@ -38,6 +38,12 @@ use crate::{
     util::ansi_texture,
 };
 
+mod atlas;
+pub use atlas::{make_texture_atlas, Atlas};
+
+//mod old_atlas;
+//pub use old_atlas::{make_texture_atlas, RenderedAtlas};
+
 // From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
 //
 // * An `FT_Face' object can only be safely used from one thread at
@@ -63,110 +69,6 @@ use crate::{
 // * Glyph cache. Key is (glyph_id, font_size)
 // * Glyph texture cache: (glyph_id, font_size, color)
 
-pub struct RenderedAtlas {
-    pub uv_rects: Vec<Rectangle>,
-    pub texture_id: TextureId,
-}
-
-const ATLAS_GAP: usize = 2;
-
-pub async fn make_texture_atlas(
-    render_api: &RenderApi,
-    font_size: f32,
-    glyphs: &Vec<Glyph>,
-) -> Result<RenderedAtlas> {
-    // First compute total size of the atlas
-    let mut total_width = ATLAS_GAP;
-    let mut total_height = ATLAS_GAP;
-
-    // Glyph IDs already rendered so we don't do it twice
-    let mut rendered = vec![];
-
-    for (idx, glyph) in glyphs.iter().enumerate() {
-        let sprite = &glyph.sprite;
-        assert_eq!(sprite.bmp.len(), 4 * sprite.bmp_width * sprite.bmp_height);
-
-        // Already done this one so skip
-        if rendered.contains(&glyph.glyph_id) {
-            continue
-        }
-        rendered.push(glyph.glyph_id);
-
-        total_width += sprite.bmp_width + ATLAS_GAP;
-        total_height = std::cmp::max(total_height, sprite.bmp_height);
-    }
-    total_width += ATLAS_GAP;
-    total_height += 2 * ATLAS_GAP;
-
-    // Allocate the big texture now
-    let mut atlas_bmp = vec![0; 4 * total_width * total_height];
-    // For debug lines we want a single white pixel.
-    atlas_bmp[0] = 255;
-    atlas_bmp[1] = 255;
-    atlas_bmp[2] = 255;
-    atlas_bmp[3] = 255;
-
-    // Calculate dimensions of final product first
-    let mut current_x = ATLAS_GAP;
-    let mut rendered_glyphs: Vec<u32> = vec![];
-    let mut uv_rects: Vec<Rectangle> = vec![];
-
-    for (idx, glyph) in glyphs.iter().enumerate() {
-        let sprite = &glyph.sprite;
-
-        // Did we already rendered this glyph?
-        // If so just copy the UV rect from before.
-        let mut uv_rect = None;
-        for (rendered_glyph_id, rendered_uv_rect) in rendered_glyphs.iter().zip(uv_rects.iter()) {
-            if *rendered_glyph_id == glyph.glyph_id {
-                uv_rect = Some(rendered_uv_rect.clone());
-            }
-        }
-
-        let uv_rect = match uv_rect {
-            Some(uv_rect) => uv_rect,
-            // Allocating a new glyph sprite in the atlas
-            None => {
-                copy_image(sprite, &mut atlas_bmp, total_width, current_x);
-
-                // Compute UV coords
-                let uv_rect = Rectangle {
-                    x: (ATLAS_GAP + current_x) as f32 / total_width as f32,
-                    y: ATLAS_GAP as f32 / total_height as f32,
-                    w: sprite.bmp_width as f32 / total_width as f32,
-                    h: sprite.bmp_height as f32 / total_height as f32,
-                };
-
-                current_x += sprite.bmp_width + ATLAS_GAP;
-
-                uv_rect
-            }
-        };
-
-        rendered_glyphs.push(glyph.glyph_id);
-        uv_rects.push(uv_rect);
-    }
-
-    // Finally allocate the texture
-    let texture_id =
-        render_api.new_texture(total_width as u16, total_height as u16, atlas_bmp).await?;
-
-    Ok(RenderedAtlas { uv_rects, texture_id })
-}
-
-fn copy_image(sprite: &Sprite, atlas_bmp: &mut Vec<u8>, total_width: usize, current_x: usize) {
-    for i in 0..sprite.bmp_height {
-        for j in 0..sprite.bmp_width {
-            let off_dest = 4 * ((i + ATLAS_GAP) * total_width + j + current_x + ATLAS_GAP);
-            let off_src = 4 * (i * sprite.bmp_width + j);
-            atlas_bmp[off_dest] = sprite.bmp[off_src];
-            atlas_bmp[off_dest + 1] = sprite.bmp[off_src + 1];
-            atlas_bmp[off_dest + 2] = sprite.bmp[off_src + 2];
-            atlas_bmp[off_dest + 3] = sprite.bmp[off_src + 3];
-        }
-    }
-}
-
 pub struct GlyphPositionIter<'a> {
     font_size: f32,
     glyphs: &'a Vec<Glyph>,
@@ -237,11 +139,11 @@ impl TextShaper {
 
         let mut faces = vec![];
 
-        let font_data = include_bytes!("../ibm-plex-mono-light.otf") as &[u8];
+        let font_data = include_bytes!("../../ibm-plex-mono-light.otf") as &[u8];
         let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
         faces.push(ft_face);
 
-        let font_data = include_bytes!("../NotoColorEmoji.ttf") as &[u8];
+        let font_data = include_bytes!("../../NotoColorEmoji.ttf") as &[u8];
         let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
         faces.push(ft_face);
 

+ 113 - 0
bin/darkwallet/src/text2/old_atlas.rs

@@ -0,0 +1,113 @@
+use miniquad::TextureId;
+
+use crate::{
+    error::Result,
+    gfx2::{Rectangle, RenderApi, RenderApiPtr},
+    util::ansi_texture,
+};
+
+use super::{Glyph, Sprite};
+
+pub struct RenderedAtlas {
+    pub uv_rects: Vec<Rectangle>,
+    pub texture_id: TextureId,
+}
+
+const ATLAS_GAP: usize = 2;
+
+pub async fn make_texture_atlas(
+    render_api: &RenderApi,
+    font_size: f32,
+    glyphs: &Vec<Glyph>,
+) -> Result<RenderedAtlas> {
+    // First compute total size of the atlas
+    let mut total_width = ATLAS_GAP;
+    let mut total_height = ATLAS_GAP;
+
+    // Glyph IDs already rendered so we don't do it twice
+    let mut rendered = vec![];
+
+    for (idx, glyph) in glyphs.iter().enumerate() {
+        let sprite = &glyph.sprite;
+        assert_eq!(sprite.bmp.len(), 4 * sprite.bmp_width * sprite.bmp_height);
+
+        // Already done this one so skip
+        if rendered.contains(&glyph.glyph_id) {
+            continue
+        }
+        rendered.push(glyph.glyph_id);
+
+        total_width += sprite.bmp_width + ATLAS_GAP;
+        total_height = std::cmp::max(total_height, sprite.bmp_height);
+    }
+    total_width += ATLAS_GAP;
+    total_height += 2 * ATLAS_GAP;
+
+    // Allocate the big texture now
+    let mut atlas_bmp = vec![0; 4 * total_width * total_height];
+    // For debug lines we want a single white pixel.
+    atlas_bmp[0] = 255;
+    atlas_bmp[1] = 255;
+    atlas_bmp[2] = 255;
+    atlas_bmp[3] = 255;
+
+    // Calculate dimensions of final product first
+    let mut current_x = ATLAS_GAP;
+    let mut rendered_glyphs: Vec<u32> = vec![];
+    let mut uv_rects: Vec<Rectangle> = vec![];
+
+    for (idx, glyph) in glyphs.iter().enumerate() {
+        let sprite = &glyph.sprite;
+
+        // Did we already rendered this glyph?
+        // If so just copy the UV rect from before.
+        let mut uv_rect = None;
+        for (rendered_glyph_id, rendered_uv_rect) in rendered_glyphs.iter().zip(uv_rects.iter()) {
+            if *rendered_glyph_id == glyph.glyph_id {
+                uv_rect = Some(rendered_uv_rect.clone());
+            }
+        }
+
+        let uv_rect = match uv_rect {
+            Some(uv_rect) => uv_rect,
+            // Allocating a new glyph sprite in the atlas
+            None => {
+                copy_image(sprite, &mut atlas_bmp, total_width, current_x);
+
+                // Compute UV coords
+                let uv_rect = Rectangle {
+                    x: (ATLAS_GAP + current_x) as f32 / total_width as f32,
+                    y: ATLAS_GAP as f32 / total_height as f32,
+                    w: sprite.bmp_width as f32 / total_width as f32,
+                    h: sprite.bmp_height as f32 / total_height as f32,
+                };
+
+                current_x += sprite.bmp_width + ATLAS_GAP;
+
+                uv_rect
+            }
+        };
+
+        rendered_glyphs.push(glyph.glyph_id);
+        uv_rects.push(uv_rect);
+    }
+
+    // Finally allocate the texture
+    let texture_id =
+        render_api.new_texture(total_width as u16, total_height as u16, atlas_bmp).await?;
+
+    Ok(RenderedAtlas { uv_rects, texture_id })
+}
+
+fn copy_image(sprite: &Sprite, atlas_bmp: &mut Vec<u8>, total_width: usize, current_x: usize) {
+    for i in 0..sprite.bmp_height {
+        for j in 0..sprite.bmp_width {
+            let off_dest = 4 * ((i + ATLAS_GAP) * total_width + j + current_x + ATLAS_GAP);
+            let off_src = 4 * (i * sprite.bmp_width + j);
+            atlas_bmp[off_dest] = sprite.bmp[off_src];
+            atlas_bmp[off_dest + 1] = sprite.bmp[off_src + 1];
+            atlas_bmp[off_dest + 2] = sprite.bmp[off_src + 2];
+            atlas_bmp[off_dest + 3] = sprite.bmp[off_src + 3];
+        }
+    }
+}

+ 138 - 5
bin/darkwallet/src/ui/chatview.rs

@@ -16,8 +16,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use darkfi_serial::{deserialize, Decodable, Encodable, SerialDecodable, SerialEncodable};
 use rand::{rngs::OsRng, Rng};
-use std::sync::Arc;
+use std::{
+    collections::BTreeMap,
+    sync::{Arc, Mutex as SyncMutex, Weak},
+};
 
 use crate::{
     error::Result,
@@ -31,29 +35,158 @@ use crate::{
     },
     pubsub::Subscription,
     scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId},
-    text2::{self, Glyph, GlyphPositionIter, RenderedAtlas, SpritePtr, TextShaper, TextShaperPtr},
+    text2::{self, Glyph, GlyphPositionIter, SpritePtr, TextShaper, TextShaperPtr},
     util::zip3,
 };
 
 use super::{eval_rect, get_parent_rect, read_rect, DrawUpdate, OnModify, Stoppable};
 
+#[derive(Debug, SerialEncodable, SerialDecodable)]
+pub struct ChatLine {
+    pub nick: String,
+    pub text: String,
+}
+
+type Timestamp = u32;
+
+struct RenderLine {
+    timest: Timestamp,
+    chatline: ChatLine,
+    glyphs: Vec<Glyph>,
+}
+
 pub type ChatViewPtr = Arc<ChatView>;
 
 pub struct ChatView {
+    node_id: SceneNodeId,
+    render_api: RenderApiPtr,
+    text_shaper: TextShaperPtr,
+    tree: sled::Tree,
+
+    lines: SyncMutex<Vec<RenderLine>>,
+    drawcalls: SyncMutex<Option<Vec<DrawInstruction>>>,
     dc_key: u64,
+
+    rect: PropertyPtr,
+    scroll: PropertyFloat32,
+    font_size: PropertyFloat32,
+    line_height: PropertyFloat32,
+    z_index: PropertyUint32,
 }
 
 impl ChatView {
-    pub async fn new() -> Pimpl {
-        let self_ = Arc::new(Self { dc_key: OsRng.gen() });
+    pub async fn new(
+        sg: SceneGraphPtr2,
+        node_id: SceneNodeId,
+        render_api: RenderApiPtr,
+        text_shaper: TextShaperPtr,
+        tree: sled::Tree,
+    ) -> Pimpl {
+        let scene_graph = sg.lock().await;
+        let node = scene_graph.get_node(node_id).unwrap();
+
+        let rect = node.get_property("rect").expect("ChatView::rect");
+        let scroll = PropertyFloat32::wrap(node, "scroll", 0).unwrap();
+        let font_size = PropertyFloat32::wrap(node, "font_size", 0).unwrap();
+        let line_height = PropertyFloat32::wrap(node, "line_height", 0).unwrap();
+        let z_index = PropertyUint32::wrap(node, "z_index", 0).unwrap();
+
+        drop(scene_graph);
+
+        let self_ = Arc::new_cyclic(|me: &Weak<Self>| Self {
+            node_id,
+            render_api,
+            text_shaper,
+            tree,
+
+            lines: SyncMutex::new(Vec::new()),
+            drawcalls: SyncMutex::new(None),
+            dc_key: OsRng.gen(),
+
+            rect,
+            scroll,
+            font_size,
+            line_height,
+            z_index,
+        });
+
+        self_.populate().await;
 
         Pimpl::ChatView(self_)
     }
 
+    async fn populate(&self) {
+        let mut lines = 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 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 glyphs = self.text_shaper.shape(text, self.font_size.get()).await;
+
+            lines.push(RenderLine { timest, chatline, glyphs });
+        }
+        *self.lines.lock().unwrap() = lines;
+    }
+
+    async fn regen_mesh(&self, mut clip: Rectangle) -> Vec<DrawInstruction> {
+        // 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![]
+    }
+
     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(mut rect) = read_rect(self.rect.clone()) else {
+            panic!("Node {:?} bad rect property", node);
+        };
+
+        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 off_x = rect.x / parent_rect.w;
+        let off_y = rect.y / parent_rect.h;
+        let scale_x = 1. / parent_rect.w;
+        let scale_y = 1. / parent_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::ApplyMatrix(model)];
+        instrs.append(&mut drawcalls);
+
         Some(DrawUpdate {
             key: self.dc_key,
-            draw_calls: vec![(self.dc_key, DrawCall { instrs: vec![], dcs: vec![], z_index: 0 })],
+            draw_calls: vec![(
+                self.dc_key,
+                DrawCall { instrs, dcs: vec![], z_index: self.z_index.get() },
+            )],
         })
     }
 }

+ 12 - 6
bin/darkwallet/src/ui/editbox.rs

@@ -39,7 +39,7 @@ use crate::{
     },
     pubsub::Subscription,
     scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId},
-    text2::{self, Glyph, GlyphPositionIter, RenderedAtlas, SpritePtr, TextShaper, TextShaperPtr},
+    text2::{self, Glyph, GlyphPositionIter, SpritePtr, TextShaper, TextShaperPtr},
     util::zip3,
 };
 
@@ -365,7 +365,7 @@ impl EditBox {
         debug!(target: "ui::editbox", "Rendering text '{}' clip={:?}", text, clip);
 
         let glyphs = self.glyphs.lock().unwrap().clone();
-        let atlas = text2::make_texture_atlas(&self.render_api, font_size, &glyphs).await.unwrap();
+        let atlas = text2::make_texture_atlas(&self.render_api, &glyphs).await.unwrap();
 
         let mut mesh = MeshBuilder::with_clip(clip.clone());
         self.draw_selected(&mut mesh, &glyphs, clip.h).unwrap();
@@ -374,9 +374,9 @@ impl EditBox {
         // Used for drawing the cursor when it's at the end of the line.
         let mut rhs = 0.;
 
-        for (glyph_idx, uv_rect, mut glyph_rect, glyph) in
-            zip3(atlas.uv_rects.into_iter(), glyph_pos_iter, glyphs.iter())
-        {
+        for (glyph_idx, (mut glyph_rect, glyph)) in glyph_pos_iter.zip(glyphs.iter()).enumerate() {
+            let uv_rect = atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
+
             glyph_rect.x += scroll;
 
             //mesh.draw_outline(&glyph_rect, COLOR_BLUE, 2.);
@@ -384,7 +384,7 @@ impl EditBox {
             if glyph.sprite.has_color {
                 color = COLOR_WHITE;
             }
-            mesh.draw_box(&glyph_rect, color, &uv_rect);
+            mesh.draw_box(&glyph_rect, color, uv_rect);
 
             if is_focused && cursor_pos != 0 && cursor_pos == glyph_idx {
                 let cursor_rect =
@@ -1217,6 +1217,12 @@ impl EditBox {
         rect.x += parent_rect.x;
         rect.y += parent_rect.y;
 
+        // We do this here because that's when we finally have the accurate rect
+        // For drawing we want the rect to be correct.
+        // TODO: parent rect changing should update this cache
+        //       do we have to subscribe to parent rect and set this None? meh
+        //       or is it better that parent can invalidate child somehow and force redraw?
+        // TODO: store drawcalls directly
         let render_info = self.render_info.lock().unwrap().clone();
         let render_info = match render_info {
             Some(render_info) => render_info,

+ 1 - 1
bin/darkwallet/src/ui/mod.rs

@@ -26,7 +26,7 @@ use crate::{
     scene::{SceneGraph, SceneNode, SceneNodeId, SceneNodeType},
 };
 
-mod chatview;
+pub mod chatview;
 pub use chatview::{ChatView, ChatViewPtr};
 mod editbox;
 pub use editbox::{EditBox, EditBoxPtr};

+ 6 - 6
bin/darkwallet/src/ui/text.rs

@@ -28,7 +28,7 @@ use crate::{
         PropertyBool, PropertyColor, PropertyFloat32, PropertyPtr, PropertyStr, PropertyUint32,
     },
     scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId},
-    text2::{self, Glyph, GlyphPositionIter, RenderedAtlas, SpritePtr, TextShaper, TextShaperPtr},
+    text2::{self, Glyph, GlyphPositionIter, SpritePtr, TextShaper, TextShaperPtr},
     util::zip3,
 };
 
@@ -135,19 +135,19 @@ impl Text {
     ) -> TextRenderInfo {
         debug!(target: "ui::text", "Rendering label '{}'", text);
         let glyphs = text_shaper.shape(text, font_size).await;
-        let atlas = text2::make_texture_atlas(render_api, font_size, &glyphs).await.unwrap();
+        let atlas = text2::make_texture_atlas(render_api, &glyphs).await.unwrap();
 
         let mut mesh = MeshBuilder::new();
         let mut glyph_pos_iter = GlyphPositionIter::new(font_size, &glyphs, baseline);
-        for (_, uv_rect, glyph_rect, glyph) in
-            zip3(atlas.uv_rects.into_iter(), glyph_pos_iter, glyphs.iter())
-        {
+        for (glyph_rect, glyph) in glyph_pos_iter.zip(glyphs.iter()) {
+            let uv_rect = atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
+
             //mesh.draw_outline(&glyph_rect, COLOR_BLUE, 2.);
             let mut color = text_color.clone();
             if glyph.sprite.has_color {
                 color = COLOR_WHITE;
             }
-            mesh.draw_box(&glyph_rect, color, &uv_rect);
+            mesh.draw_box(&glyph_rect, color, uv_rect);
         }
 
         let mesh = mesh.alloc(&render_api).await.unwrap();