Просмотр исходного кода

wallet: add text rendering and a label widget

darkfi 2 лет назад
Родитель
Сommit
eb2241ed13

+ 1 - 0
bin/darkwallet/Cargo.toml

@@ -36,6 +36,7 @@ rand = "0.8.5"
 async-lock = "3.4.0"
 async-lock = "3.4.0"
 futures = "0.3.30"
 futures = "0.3.30"
 async-recursion = "1.1.1"
 async-recursion = "1.1.1"
+colored = "2.1.0"
 
 
 #rustpython-vm = "0.3.1"
 #rustpython-vm = "0.3.1"
 
 

+ 2 - 1
bin/darkwallet/echo.py

@@ -5,7 +5,8 @@ from pydrk import serial
 context = zmq.Context()
 context = zmq.Context()
 socket = context.socket(zmq.REQ)
 socket = context.socket(zmq.REQ)
 #self.socket.setsockopt(zmq.IPV6, True)
 #self.socket.setsockopt(zmq.IPV6, True)
-socket.connect(f"tcp://127.0.0.1:9484")
+#socket.connect(f"tcp://127.0.0.1:9484")
+socket.connect(f"tcp://192.168.1.20:9484")
 
 
 req_cmd = bytearray()
 req_cmd = bytearray()
 serial.write_u8(req_cmd, 0)
 serial.write_u8(req_cmd, 0)

+ 1 - 1
bin/darkwallet/gui/api.py

@@ -2,7 +2,7 @@ from collections import namedtuple
 from pydrk import Api, HostApi, PropertyType, PropertySubType, Property, serial
 from pydrk import Api, HostApi, PropertyType, PropertySubType, Property, serial
 import zmq
 import zmq
 
 
-api = Api()
+api = Api(addr="192.168.1.20")
 host = HostApi(api)
 host = HostApi(api)
 print("Node status:", api.hello())
 print("Node status:", api.hello())
 
 

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

@@ -7,7 +7,8 @@ use crate::{
     gfx2::{GraphicsEventPublisherPtr, RenderApiPtr, Vertex},
     gfx2::{GraphicsEventPublisherPtr, RenderApiPtr, Vertex},
     prop::{Property, PropertySubType, PropertyType},
     prop::{Property, PropertySubType, PropertyType},
     scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId, SceneNodeType},
     scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId, SceneNodeType},
-    ui::{Mesh, RenderLayer, Stoppable, Window},
+    text2::TextShaperPtr,
+    ui::{Mesh, RenderLayer, Stoppable, Text, Window},
 };
 };
 
 
 //fn print_type_of<T>(_: &T) {
 //fn print_type_of<T>(_: &T) {
@@ -86,6 +87,7 @@ pub struct App {
     ex: Arc<smol::Executor<'static>>,
     ex: Arc<smol::Executor<'static>>,
     render_api: RenderApiPtr,
     render_api: RenderApiPtr,
     event_pub: GraphicsEventPublisherPtr,
     event_pub: GraphicsEventPublisherPtr,
+    text_shaper: TextShaperPtr,
 }
 }
 
 
 impl App {
 impl App {
@@ -94,12 +96,13 @@ impl App {
         ex: Arc<smol::Executor<'static>>,
         ex: Arc<smol::Executor<'static>>,
         render_api: RenderApiPtr,
         render_api: RenderApiPtr,
         event_pub: GraphicsEventPublisherPtr,
         event_pub: GraphicsEventPublisherPtr,
+        text_shaper: TextShaperPtr,
     ) -> Arc<Self> {
     ) -> Arc<Self> {
-        Arc::new(Self { sg, ex, render_api, event_pub })
+        Arc::new(Self { sg, ex, render_api, event_pub, text_shaper })
     }
     }
 
 
     pub async fn start(self: Arc<Self>) {
     pub async fn start(self: Arc<Self>) {
-        debug!("App::start()");
+        debug!(target: "app", "App::start()");
         // Setup UI
         // Setup UI
         let mut sg = self.sg.lock().await;
         let mut sg = self.sg.lock().await;
 
 
@@ -252,7 +255,6 @@ impl App {
         prop.set_f32(3, 60.).unwrap();
         prop.set_f32(3, 60.).unwrap();
 
 
         // Setup the pimpl
         // Setup the pimpl
-        let node_id = node.id;
         let (x1, y1) = (0., 0.);
         let (x1, y1) = (0., 0.);
         let (x2, y2) = (1., 1.);
         let (x2, y2) = (1., 1.);
         let verts = vec![
         let verts = vec![
@@ -281,6 +283,40 @@ impl App {
         node.pimpl = pimpl;
         node.pimpl = pimpl;
 
 
         sg.link(node_id, layer_node_id).unwrap();
         sg.link(node_id, layer_node_id).unwrap();
+
+        // Create some text
+        let node_id = create_text(&mut sg, "label");
+
+        let node = sg.get_node_mut(node_id).unwrap();
+        let prop = node.get_property("rect").unwrap();
+        prop.set_f32(0, 100.).unwrap();
+        prop.set_f32(1, 100.).unwrap();
+        prop.set_f32(2, 800.).unwrap();
+        prop.set_f32(3, 200.).unwrap();
+        node.set_property_f32("baseline", 40.).unwrap();
+        node.set_property_f32("font_size", 20.).unwrap();
+        //node.set_property_str("text", "anon1🍆").unwrap();
+        node.set_property_str("text", "anon1").unwrap();
+        let prop = node.get_property("color").unwrap();
+        prop.set_f32(0, 0.).unwrap();
+        prop.set_f32(1, 1.).unwrap();
+        prop.set_f32(2, 0.).unwrap();
+        prop.set_f32(3, 1.).unwrap();
+
+        drop(sg);
+        let pimpl = Text::new(
+            self.ex.clone(),
+            self.sg.clone(),
+            node_id,
+            self.render_api.clone(),
+            self.text_shaper.clone(),
+        )
+        .await;
+        let mut sg = self.sg.lock().await;
+        let node = sg.get_node_mut(node_id).unwrap();
+        node.pimpl = pimpl;
+
+        sg.link(node_id, layer_node_id).unwrap();
     }
     }
 
 
     async fn trigger_redraw(&self) {
     async fn trigger_redraw(&self) {
@@ -319,3 +355,34 @@ pub fn create_mesh(sg: &mut SceneGraph, name: &str) -> SceneNodeId {
 
 
     node.id
     node.id
 }
 }
+
+fn create_text(sg: &mut SceneGraph, name: &str) -> SceneNodeId {
+    let node = sg.add_node(name, SceneNodeType::RenderText);
+
+    let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
+    prop.set_array_len(4);
+    prop.allow_exprs();
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("baseline", PropertyType::Float32, PropertySubType::Pixel);
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("font_size", PropertyType::Float32, PropertySubType::Pixel);
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("text", PropertyType::Str, PropertySubType::Null);
+    node.add_property(prop).unwrap();
+
+    let mut prop = Property::new("color", PropertyType::Float32, PropertySubType::Color);
+    prop.set_array_len(4);
+    prop.set_range_f32(0., 1.);
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("z_index", PropertyType::Uint32, PropertySubType::Null);
+    node.add_property(prop).unwrap();
+
+    let prop = Property::new("debug", PropertyType::Bool, PropertySubType::Null);
+    node.add_property(prop).unwrap();
+
+    node.id
+}

+ 12 - 3
bin/darkwallet/src/gfx2.rs

@@ -17,6 +17,7 @@ use crate::{
     error::{Error, Result},
     error::{Error, Result},
     pubsub::{Publisher, PublisherPtr, Subscription},
     pubsub::{Publisher, PublisherPtr, Subscription},
     shader,
     shader,
+    util::ansi_texture,
 };
 };
 
 
 // This is very noisy so suppress output by default
 // This is very noisy so suppress output by default
@@ -109,7 +110,7 @@ impl RenderApi {
         Arc::new(Self { method_req })
         Arc::new(Self { method_req })
     }
     }
 
 
-    async fn new_texture(&self, width: u16, height: u16, data: Vec<u8>) -> Result<TextureId> {
+    pub async fn new_texture(&self, width: u16, height: u16, data: Vec<u8>) -> Result<TextureId> {
         let (sendr, recvr) = async_channel::bounded(1);
         let (sendr, recvr) = async_channel::bounded(1);
 
 
         let method = GraphicsMethod::NewTexture((width, height, data, sendr));
         let method = GraphicsMethod::NewTexture((width, height, data, sendr));
@@ -120,7 +121,7 @@ impl RenderApi {
         Ok(texture_id)
         Ok(texture_id)
     }
     }
 
 
-    fn delete_texture(&self, texture: TextureId) {
+    pub fn delete_texture(&self, texture: TextureId) {
         let method = GraphicsMethod::DeleteTexture(texture);
         let method = GraphicsMethod::DeleteTexture(texture);
 
 
         // Ignore any error
         // Ignore any error
@@ -394,9 +395,13 @@ impl Stage {
         sendr: async_channel::Sender<TextureId>,
         sendr: async_channel::Sender<TextureId>,
     ) {
     ) {
         let texture = self.ctx.new_texture_from_rgba8(width, height, &data);
         let texture = self.ctx.new_texture_from_rgba8(width, height, &data);
+        debug!(target: "gfx2", "Invoked method: new_texture({}, {}, ...) -> {:?}\n{}",
+               width, height, texture,
+               ansi_texture(width as usize, height as usize, &data));
         sendr.try_send(texture).unwrap();
         sendr.try_send(texture).unwrap();
     }
     }
     fn method_delete_texture(&mut self, texture: TextureId) {
     fn method_delete_texture(&mut self, texture: TextureId) {
+        debug!(target: "gfx2", "Invoked method: delete_texture({:?})", texture);
         self.ctx.delete_texture(texture);
         self.ctx.delete_texture(texture);
     }
     }
     fn method_new_vertex_buffer(
     fn method_new_vertex_buffer(
@@ -409,6 +414,7 @@ impl Stage {
             BufferUsage::Immutable,
             BufferUsage::Immutable,
             BufferSource::slice(&verts),
             BufferSource::slice(&verts),
         );
         );
+        debug!(target: "gfx2", "Invoked method: new_vertex_buffer({:?}) -> {:?}", verts, buffer);
         sendr.try_send(buffer).unwrap();
         sendr.try_send(buffer).unwrap();
     }
     }
     fn method_new_index_buffer(
     fn method_new_index_buffer(
@@ -421,12 +427,15 @@ impl Stage {
             BufferUsage::Immutable,
             BufferUsage::Immutable,
             BufferSource::slice(&indices),
             BufferSource::slice(&indices),
         );
         );
+        debug!(target: "gfx2", "Invoked method: new_index_buffer({:?}) -> {:?}", indices, buffer);
         sendr.try_send(buffer).unwrap();
         sendr.try_send(buffer).unwrap();
     }
     }
     fn method_delete_buffer(&mut self, buffer: BufferId) {
     fn method_delete_buffer(&mut self, buffer: BufferId) {
+        debug!(target: "gfx2", "Invoked method: delete_buffer({:?})", buffer);
         self.ctx.delete_buffer(buffer);
         self.ctx.delete_buffer(buffer);
     }
     }
     fn method_replace_draw_calls(&mut self, dcs: Vec<(u64, DrawCall)>) {
     fn method_replace_draw_calls(&mut self, dcs: Vec<(u64, DrawCall)>) {
+        debug!(target: "gfx2", "Invoked method: replace_draw_calls({:?})", dcs);
         for (key, val) in dcs {
         for (key, val) in dcs {
             self.draw_calls.insert(key, val);
             self.draw_calls.insert(key, val);
         }
         }
@@ -452,7 +461,7 @@ impl EventHandler for Stage {
 
 
         loop {
         loop {
             let Ok(method) = self.method_rep.recv_deadline(deadline) else { break };
             let Ok(method) = self.method_rep.recv_deadline(deadline) else { break };
-            debug!(target: "gfx", "Received method: {:?}", method);
+            //debug!(target: "gfx", "Received method: {:?}", method);
             match method {
             match method {
                 GraphicsMethod::NewTexture((width, height, data, sendr)) => {
                 GraphicsMethod::NewTexture((width, height, data, sendr)) => {
                     self.method_new_texture(width, height, data, sendr)
                     self.method_new_texture(width, height, data, sendr)

+ 10 - 3
bin/darkwallet/src/main.rs

@@ -30,10 +30,11 @@ mod pubsub;
 //mod res;
 //mod res;
 mod scene;
 mod scene;
 mod shader;
 mod shader;
-//mod text;
+mod text2;
 mod ui;
 mod ui;
+mod util;
 
 
-use crate::{net::ZeroMQAdapter, scene::SceneGraph};
+use crate::{net::ZeroMQAdapter, scene::SceneGraph, text2::TextShaper};
 
 
 #[cfg(target_os = "android")]
 #[cfg(target_os = "android")]
 fn panic_hook(panic_info: &std::panic::PanicInfo) {
 fn panic_hook(panic_info: &std::panic::PanicInfo) {
@@ -53,6 +54,9 @@ fn main() {
 
 
     #[cfg(target_os = "linux")]
     #[cfg(target_os = "linux")]
     {
     {
+        // For ANSI colors in the terminal
+        colored::control::set_override(true);
+
         let term_logger = simplelog::TermLogger::new(
         let term_logger = simplelog::TermLogger::new(
             simplelog::LevelFilter::Debug,
             simplelog::LevelFilter::Debug,
             simplelog::Config::default(),
             simplelog::Config::default(),
@@ -82,7 +86,10 @@ fn main() {
     let render_api = gfx2::RenderApi::new(method_req);
     let render_api = gfx2::RenderApi::new(method_req);
     let event_pub = gfx2::GraphicsEventPublisher::new();
     let event_pub = gfx2::GraphicsEventPublisher::new();
 
 
-    let app = app::App::new(sg.clone(), ex.clone(), render_api.clone(), event_pub.clone());
+    let text_shaper = TextShaper::new();
+
+    let app =
+        app::App::new(sg.clone(), ex.clone(), render_api.clone(), event_pub.clone(), text_shaper);
     let app_task = ex.spawn(app.start());
     let app_task = ex.spawn(app.start());
     async_runtime.push_task(app_task);
     async_runtime.push_task(app_task);
     //app.clone().start();
     //app.clone().start();

+ 1 - 0
bin/darkwallet/src/scene.rs

@@ -632,6 +632,7 @@ pub enum Pimpl {
     Window(ui::WindowPtr),
     Window(ui::WindowPtr),
     RenderLayer(ui::RenderLayerPtr),
     RenderLayer(ui::RenderLayerPtr),
     Mesh(ui::MeshPtr),
     Mesh(ui::MeshPtr),
+    Text(ui::TextPtr),
 }
 }
 
 
 impl std::fmt::Debug for SceneNode {
 impl std::fmt::Debug for SceneNode {

+ 26 - 0
bin/darkwallet/src/text.rs

@@ -2,6 +2,31 @@ use freetype as ft;
 
 
 use crate::gfx::{FreetypeFace, Rectangle};
 use crate::gfx::{FreetypeFace, Rectangle};
 
 
+// From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
+//
+// * An `FT_Face' object can only be safely used from one thread at
+//   a time.
+//
+// * An `FT_Library'  object can  now be used  without modification
+//   from multiple threads at the same time.
+//
+// * `FT_Face' creation and destruction  with the same `FT_Library'
+//   object can only be done from one thread at a time.
+//
+// One can use a single  `FT_Library' object across threads as long
+// as a mutex lock is used around `FT_New_Face' and `FT_Done_Face'.
+// Any calls to `FT_Load_Glyph' and similar API are safe and do not
+// need the lock  to be held as  long as the same  `FT_Face' is not
+// used from multiple threads at the same time.
+
+// Harfbuzz is threadsafe.
+
+// Notes:
+// * All ft init and face creation should happen at startup.
+// * FT faces protected behind an async Mutex
+// * Glyph cache. Key is (glyph_id, font_size)
+// * Glyph texture cache: (glyph_id, font_size, color)
+
 #[derive(Clone)]
 #[derive(Clone)]
 pub struct Glyph {
 pub struct Glyph {
     pub id: u32,
     pub id: u32,
@@ -163,6 +188,7 @@ impl TextShaper {
                     let w = (bmp_width as f32 * font_size) / bmp_height as f32;
                     let w = (bmp_width as f32 * font_size) / bmp_height as f32;
                     let h = font_size;
                     let h = font_size;
 
 
+                    // Shouldn't this use the bearing?
                     let x = current_x;
                     let x = current_x;
                     let y = current_y - h;
                     let y = current_y - h;
 
 

+ 472 - 0
bin/darkwallet/src/text2.rs

@@ -0,0 +1,472 @@
+use async_lock::Mutex;
+use freetype as ft;
+use miniquad::TextureId;
+use std::{
+    collections::HashMap,
+    sync::{Arc, Weak},
+};
+
+use crate::{
+    error::Result,
+    gfx2::{Rectangle, RenderApiPtr},
+    util::ansi_texture,
+};
+
+// From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
+//
+// * An `FT_Face' object can only be safely used from one thread at
+//   a time.
+//
+// * An `FT_Library'  object can  now be used  without modification
+//   from multiple threads at the same time.
+//
+// * `FT_Face' creation and destruction  with the same `FT_Library'
+//   object can only be done from one thread at a time.
+//
+// One can use a single  `FT_Library' object across threads as long
+// as a mutex lock is used around `FT_New_Face' and `FT_Done_Face'.
+// Any calls to `FT_Load_Glyph' and similar API are safe and do not
+// need the lock  to be held as  long as the same  `FT_Face' is not
+// used from multiple threads at the same time.
+
+// Harfbuzz is threadsafe.
+
+// Notes:
+// * All ft init and face creation should happen at startup.
+// * FT faces protected behind an async Mutex
+// * 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 = 1;
+
+pub async fn make_texture_atlas(
+    render_api: RenderApiPtr,
+    font_size: f32,
+    glyphs: &Vec<Glyph>,
+) -> Result<RenderedAtlas> {
+    let mut total_width = ATLAS_GAP;
+    let mut total_height = ATLAS_GAP;
+    for glyph in glyphs {
+        let sprite = &glyph.sprite;
+        assert_eq!(sprite.bmp.len(), 4 * sprite.bmp_width * sprite.bmp_height);
+
+        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;
+
+    let mut atlas_bmp = vec![0; 4 * total_width * total_height];
+
+    // Calculate dimensions of final product first
+    let mut current_x = ATLAS_GAP;
+    let mut uv_rects = vec![];
+
+    for glyph in glyphs {
+        let sprite = &glyph.sprite;
+
+        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];
+            }
+        }
+
+        // Compute UV coords
+        let uv_rect = Rectangle {
+            x: current_x as f32 / total_width as f32,
+            y: 0.,
+            w: sprite.bmp_width as f32 / total_width as f32,
+            h: sprite.bmp_height as f32 / total_height as f32,
+        };
+        uv_rects.push(uv_rect);
+
+        current_x += sprite.bmp_width + ATLAS_GAP;
+    }
+
+    let texture_id =
+        render_api.new_texture(total_width as u16, total_height as u16, atlas_bmp).await?;
+
+    Ok(RenderedAtlas { uv_rects, texture_id })
+}
+
+pub struct TextShaper {
+    font_faces: Mutex<FtFaces>,
+    cache: Mutex<TextShaperCache>,
+}
+
+impl TextShaper {
+    pub fn new() -> Arc<Self> {
+        let ftlib = ft::Library::init().unwrap();
+
+        let mut faces = vec![];
+
+        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 ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
+        faces.push(ft_face);
+
+        Arc::new(Self { font_faces: Mutex::new(FtFaces(faces)), cache: Mutex::new(HashMap::new()) })
+    }
+
+    pub fn split_into_substrs(
+        font_faces: &Vec<FreetypeFace>,
+        text: String,
+    ) -> Vec<(usize, String)> {
+        let mut current_idx = 0;
+        let mut current_str = String::new();
+        let mut substrs = vec![];
+        'next_char: for chr in text.chars() {
+            let idx = 'get_idx: {
+                for i in 0..font_faces.len() {
+                    let ft_face = &font_faces[i];
+                    if ft_face.get_char_index(chr as usize).is_some() {
+                        break 'get_idx i
+                    }
+                }
+                drop(font_faces);
+
+                warn!(target: "text", "no font fallback for char: '{}'", chr);
+                // Skip this char
+                continue 'next_char
+            };
+            if current_idx != idx {
+                if !current_str.is_empty() {
+                    // Push
+                    substrs.push((current_idx, current_str.clone()));
+                }
+
+                current_str.clear();
+                current_idx = idx;
+            }
+            current_str.push(chr);
+        }
+        if !current_str.is_empty() {
+            // Push
+            substrs.push((current_idx, current_str));
+        }
+        substrs
+    }
+
+    /*
+    pub async fn shape(&self, text: String, font_size: f32) -> Result<Vec<Glyph>> {
+        let preglyphs = self.preshape(text, font_size).await;
+
+        let mut glyphs = vec![];
+        for preglyph in preglyphs {
+            let texture = match preglyph.texture {
+                PreGlyphTexture::Cached(texture) => texture,
+                PreGlyphTexture::Uncached(raw_texture) => self.cache_texture(preglyph.glyph_id, font_size, preglyph.face_idx, raw_texture).await?,
+            };
+
+            let glyph = Glyph {
+                glyph_id: preglyph.glyph_id,
+                substr: preglyph.substr,
+                texture,
+                x_offset: preglyph.x_offset,
+                y_offset: preglyph.y_offset,
+                x_advance: preglyph.x_advance,
+                y_advance: preglyph.y_advance,
+            };
+
+            glyphs.push(glyph);
+        }
+        Ok(glyphs)
+    }
+        */
+
+    /*
+    async fn cache_texture(&self, glyph_id: u32, font_size: f32, face_idx: usize, raw_texture: RawTextureData) -> Result<ManagedTexturePtr> {
+        let cache_key = CacheKey {
+            glyph_id,
+            font_size: if raw_texture.has_fixed_sizes {
+                FontSize::Fixed
+            } else {
+                FontSize::from(font_size)
+            },
+            face_idx,
+        };
+
+        // Not an issue to lock this intermittently preshape will request a glyph
+        // to be cached. The worst that can happen is we double cache a glyph which
+        // is no big deal.
+        // Ofc we should benchmark this to see how well it performs in practice.
+        let mut cache = self.cache.lock().await;
+
+        // If we're shaping the a string like "anon1", then n will be cached twice
+        // So lets check again if it exists first.
+        if let Some(texture) = cache.get(&cache_key) {
+            if let Some(texture) = texture.upgrade() {
+                return Ok(texture)
+            }
+        }
+
+        let texture_id = self
+            .render_api
+            .new_texture(
+                raw_texture.bmp_width as u16,
+                raw_texture.bmp_height as u16,
+                raw_texture.bmp,
+            )
+            .await?;
+
+        let texture = Arc::new(ManagedTexture {
+            texture_id,
+            render_api: self.render_api.clone(),
+            bmp_width: raw_texture.bmp_width,
+            bmp_height: raw_texture.bmp_height,
+            bearing_x: raw_texture.bearing_x,
+            bearing_y: raw_texture.bearing_y,
+            has_fixed_sizes: raw_texture.has_fixed_sizes,
+        });
+
+        cache.insert(cache_key, Arc::downgrade(&texture));
+
+        Ok(texture)
+    }
+    */
+
+    pub async fn shape(&self, text: String, font_size: f32) -> Vec<Glyph> {
+        // Lock font faces
+        // Freetype faces are not threadsafe
+        let faces = self.font_faces.lock().await;
+        let mut cache = self.cache.lock().await;
+
+        let substrs = Self::split_into_substrs(&faces.0, text.clone());
+
+        let mut glyphs: Vec<Glyph> = vec![];
+
+        let mut current_x = 0.;
+        let mut current_y = 0.;
+
+        for (face_idx, text) in substrs {
+            //debug!("substr {}", text);
+            let face = &faces.0[face_idx];
+            if face.has_fixed_sizes() {
+                // emojis required a fixed size
+                //face.set_char_size(109 * 64, 0, 72, 72).unwrap();
+                face.select_size(0).unwrap();
+            } else {
+                face.set_char_size(font_size as isize * 64, 0, 72, 72).unwrap();
+            }
+
+            let hb_font = harfbuzz_rs::Font::from_freetype_face(face.clone());
+            let buffer = harfbuzz_rs::UnicodeBuffer::new()
+                .set_cluster_level(harfbuzz_rs::ClusterLevel::MonotoneCharacters)
+                .add_str(&text);
+            let output = harfbuzz_rs::shape(&hb_font, buffer, &[]);
+
+            let positions = output.get_glyph_positions();
+            let infos = output.get_glyph_infos();
+
+            let mut prev_cluster = 0;
+
+            for (i, (position, info)) in positions.iter().zip(infos).enumerate() {
+                let glyph_id = info.codepoint;
+                // Index within this substr
+                let curr_cluster = info.cluster as usize;
+
+                // Skip first time
+                if i != 0 {
+                    let substr = text[prev_cluster..curr_cluster].to_string();
+                    glyphs.last_mut().unwrap().substr = substr;
+                }
+
+                prev_cluster = curr_cluster;
+
+                let x_offset = position.x_offset as f32 / 64.;
+                let y_offset = position.y_offset as f32 / 64.;
+                let x_advance = position.x_advance as f32 / 64.;
+                let y_advance = position.y_advance as f32 / 64.;
+
+                // Check cache
+                // If it exists in the cache then skip
+                // Relevant info:
+                // * glyph_id
+                // * font_size (for non-fixed size faces)
+                // * face_idx
+                let cache_key = CacheKey {
+                    glyph_id,
+                    font_size: if face.has_fixed_sizes() {
+                        FontSize::Fixed
+                    } else {
+                        FontSize::from(font_size)
+                    },
+                    face_idx,
+                };
+                //debug!(target: "text", "cache_key: {:?}", cache_key);
+                if let Some(sprite) = cache.get(&cache_key) {
+                    let Some(sprite) = sprite.upgrade() else { break };
+
+                    let glyph = Glyph {
+                        glyph_id,
+                        substr: String::new(),
+                        sprite,
+                        x_offset,
+                        y_offset,
+                        x_advance,
+                        y_advance,
+                    };
+
+                    glyphs.push(glyph);
+                    continue
+                }
+
+                let mut flags = ft::face::LoadFlag::DEFAULT;
+                if face.has_color() {
+                    flags |= ft::face::LoadFlag::COLOR;
+                }
+
+                // FIXME: glyph 884 hangs on android
+                // For now just avoid using emojis on android
+                //debug!("load_glyph {}", gid);
+                face.load_glyph(glyph_id, flags).unwrap();
+                //debug!("load_glyph {} [done]", gid);
+
+                let glyph = face.glyph();
+                glyph.render_glyph(ft::RenderMode::Normal).unwrap();
+
+                let bmp = glyph.bitmap();
+                let buffer = bmp.buffer();
+                let bmp_width = bmp.width() as usize;
+                let bmp_height = bmp.rows() as usize;
+                let bearing_x = glyph.bitmap_left() as f32;
+                let bearing_y = glyph.bitmap_top() as f32;
+                let has_fixed_sizes = face.has_fixed_sizes();
+
+                let pixel_mode = bmp.pixel_mode().unwrap();
+                let bmp = match pixel_mode {
+                    ft::bitmap::PixelMode::Bgra => {
+                        let mut tdata = vec![];
+                        tdata.resize(4 * bmp_width * bmp_height, 0);
+                        // Convert from BGRA to RGBA
+                        for i in 0..bmp_width * bmp_height {
+                            let idx = i * 4;
+                            let b = buffer[idx];
+                            let g = buffer[idx + 1];
+                            let r = buffer[idx + 2];
+                            let a = buffer[idx + 3];
+                            tdata[idx] = r;
+                            tdata[idx + 1] = g;
+                            tdata[idx + 2] = b;
+                            tdata[idx + 3] = a;
+                        }
+                        tdata
+                    }
+                    ft::bitmap::PixelMode::Gray => {
+                        // Convert from greyscale to RGBA8
+                        let tdata: Vec<_> = buffer
+                            .iter()
+                            .flat_map(|coverage| {
+                                let r = 255;
+                                let g = 255;
+                                let b = 255;
+                                let α = ((*coverage as f32) * 255.) as u8;
+                                vec![r, g, b, α]
+                            })
+                            .collect();
+                        tdata
+                    }
+                    _ => panic!("unsupport pixel mode: {:?}", pixel_mode),
+                };
+
+                let sprite = Arc::new(Sprite {
+                    bmp,
+                    bmp_width,
+                    bmp_height,
+                    bearing_x,
+                    bearing_y,
+                    has_fixed_sizes,
+                });
+
+                cache.insert(cache_key, Arc::downgrade(&sprite));
+
+                let glyph = Glyph {
+                    glyph_id,
+                    substr: String::new(),
+                    sprite,
+                    x_offset,
+                    y_offset,
+                    x_advance,
+                    y_advance,
+                };
+
+                glyphs.push(glyph);
+            }
+
+            let substr = text[prev_cluster..].to_string();
+            glyphs.last_mut().unwrap().substr = substr;
+        }
+
+        glyphs
+    }
+}
+
+#[derive(Eq, Hash, PartialEq, Debug)]
+enum FontSize {
+    Fixed,
+    Size(u32),
+}
+
+impl FontSize {
+    /// You can't use f32 in Hash and Eq impls
+    fn from(size: f32) -> Self {
+        Self::Size((size * 1000.).round() as u32)
+    }
+}
+
+#[derive(Eq, Hash, PartialEq, Debug)]
+struct CacheKey {
+    glyph_id: u32,
+    font_size: FontSize,
+    face_idx: usize,
+}
+
+type SpritePtr = Arc<Sprite>;
+
+struct Sprite {
+    bmp: Vec<u8>,
+    bmp_width: usize,
+    bmp_height: usize,
+
+    bearing_x: f32,
+    bearing_y: f32,
+    has_fixed_sizes: bool,
+}
+
+pub struct Glyph {
+    pub glyph_id: u32,
+    // Substring this glyph corresponds to
+    pub substr: String,
+
+    pub sprite: SpritePtr,
+
+    // Normally these are i32, we provide the conversions
+    pub x_offset: f32,
+    pub y_offset: f32,
+    pub x_advance: f32,
+    pub y_advance: f32,
+}
+
+type FreetypeFace = ft::Face<&'static [u8]>;
+
+struct FtFaces(Vec<FreetypeFace>);
+
+unsafe impl Send for FtFaces {}
+unsafe impl Sync for FtFaces {}
+
+pub type TextShaperPtr = Arc<TextShaper>;
+
+type TextShaperCache = HashMap<CacheKey, Weak<Sprite>>;

+ 13 - 11
bin/darkwallet/src/ui/layer.rs

@@ -68,20 +68,21 @@ impl RenderLayer {
             return;
             return;
         };
         };
 
 
-        let Some(draw_update) = self.draw(&sg, &parent_rect) else {
-            error!("RenderLayer {:?} failed to draw", node);
+        let Some(draw_update) = self.draw(&sg, &parent_rect).await else {
+            error!(target: "ui::layer", "RenderLayer {:?} failed to draw", node);
             return;
             return;
         };
         };
         self.render_api.replace_draw_calls(draw_update.draw_calls).await;
         self.render_api.replace_draw_calls(draw_update.draw_calls).await;
-        debug!("replace draw calls done");
+        debug!(target: "ui::layer", "replace draw calls done");
     }
     }
 
 
-    pub fn draw(&self, sg: &SceneGraph, parent_rect: &Rectangle) -> Option<DrawUpdate> {
-        debug!(target: "app", "RenderLayer::draw()");
+    #[async_recursion]
+    pub async fn draw(&self, sg: &SceneGraph, parent_rect: &Rectangle) -> Option<DrawUpdate> {
+        debug!(target: "ui::layer", "RenderLayer::draw()");
         let node = sg.get_node(self.node_id).unwrap();
         let node = sg.get_node(self.node_id).unwrap();
 
 
         if !self.is_visible.get() {
         if !self.is_visible.get() {
-            debug!(target: "app", "invisible layer node '{}':{}", node.name, node.id);
+            debug!(target: "ui::layer", "invisible layer node '{}':{}", node.name, node.id);
             return None
             return None
         }
         }
 
 
@@ -98,15 +99,15 @@ impl RenderLayer {
 
 
         if !parent_rect.includes(&rect) {
         if !parent_rect.includes(&rect) {
             error!(
             error!(
-                target: "app",
+                target: "ui::layer",
                 "layer '{}':{} rect {:?} is not inside parent {:?}",
                 "layer '{}':{} rect {:?} is not inside parent {:?}",
                 node.name, node.id, rect, parent_rect
                 node.name, node.id, rect, parent_rect
             );
             );
             return None
             return None
         }
         }
 
 
-        debug!(target: "app", "Parent rect: {:?}", parent_rect);
-        debug!(target: "app", "Viewport rect: {:?}", rect);
+        debug!(target: "ui::layer", "Parent rect: {:?}", parent_rect);
+        debug!(target: "ui::layer", "Viewport rect: {:?}", rect);
 
 
         // Apply viewport
         // Apply viewport
 
 
@@ -116,10 +117,11 @@ impl RenderLayer {
             let node = sg.get_node(child_inf.id).unwrap();
             let node = sg.get_node(child_inf.id).unwrap();
 
 
             let dcs = match &node.pimpl {
             let dcs = match &node.pimpl {
-                Pimpl::RenderLayer(layer) => layer.draw(&sg, &rect),
+                Pimpl::RenderLayer(layer) => layer.draw(&sg, &rect).await,
                 Pimpl::Mesh(mesh) => mesh.draw(&sg, &rect),
                 Pimpl::Mesh(mesh) => mesh.draw(&sg, &rect),
+                Pimpl::Text(txt) => txt.draw(&sg, &rect).await,
                 _ => {
                 _ => {
-                    error!(target: "app", "unhandled pimpl type");
+                    error!(target: "ui::layer", "unhandled pimpl type");
                     continue
                     continue
                 }
                 }
             };
             };

+ 5 - 5
bin/darkwallet/src/ui/mesh.rs

@@ -44,8 +44,8 @@ impl Mesh {
         let scene_graph = sg.lock().await;
         let scene_graph = sg.lock().await;
         let node = scene_graph.get_node(node_id).unwrap();
         let node = scene_graph.get_node(node_id).unwrap();
         let node_name = node.name.clone();
         let node_name = node.name.clone();
-        let rect = node.get_property("rect").expect("RenderLayer::rect");
-        let z_index_prop = node.get_property("z_index").expect("RenderLayer::z_index");
+        let rect = node.get_property("rect").expect("Mesh::rect");
+        let z_index_prop = node.get_property("z_index").expect("Mesh::z_index");
         let z_index = PropertyUint32::from(z_index_prop.clone(), 0).unwrap();
         let z_index = PropertyUint32::from(z_index_prop.clone(), 0).unwrap();
         drop(scene_graph);
         drop(scene_graph);
 
 
@@ -80,15 +80,15 @@ impl Mesh {
         };
         };
 
 
         let Some(draw_update) = self.draw(&sg, &parent_rect) else {
         let Some(draw_update) = self.draw(&sg, &parent_rect) else {
-            error!("Mesh {:?} failed to draw", node);
+            error!(target: "ui::mesh", "Mesh {:?} failed to draw", node);
             return;
             return;
         };
         };
         self.render_api.replace_draw_calls(draw_update.draw_calls).await;
         self.render_api.replace_draw_calls(draw_update.draw_calls).await;
-        debug!("replace draw calls done");
+        debug!(target: "ui::mesh", "replace draw calls done");
     }
     }
 
 
     pub fn draw(&self, sg: &SceneGraph, parent_rect: &Rectangle) -> Option<DrawUpdate> {
     pub fn draw(&self, sg: &SceneGraph, parent_rect: &Rectangle) -> Option<DrawUpdate> {
-        debug!(target: "app", "Mesh::draw()");
+        debug!(target: "ui::mesh", "Mesh::draw()");
         // Only used for debug messages
         // Only used for debug messages
         let node = sg.get_node(self.node_id).unwrap();
         let node = sg.get_node(self.node_id).unwrap();
 
 

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

@@ -12,6 +12,8 @@ mod mesh;
 pub use mesh::{Mesh, MeshPtr};
 pub use mesh::{Mesh, MeshPtr};
 mod layer;
 mod layer;
 pub use layer::{RenderLayer, RenderLayerPtr};
 pub use layer::{RenderLayer, RenderLayerPtr};
+mod text;
+pub use text::{Text, TextPtr};
 mod win;
 mod win;
 pub use win::{Window, WindowPtr};
 pub use win::{Window, WindowPtr};
 
 

+ 178 - 0
bin/darkwallet/src/ui/text.rs

@@ -0,0 +1,178 @@
+use async_lock::Mutex;
+use rand::{rngs::OsRng, Rng};
+use std::sync::{Arc, Weak};
+
+use crate::{
+    gfx2::{DrawCall, DrawInstruction, DrawMesh, Rectangle, RenderApiPtr, Vertex},
+    prop::{PropertyPtr, PropertyUint32},
+    scene::{Pimpl, SceneGraph, SceneGraphPtr2, SceneNodeId},
+    text2::{self, Glyph, RenderedAtlas, TextShaperPtr},
+};
+
+use super::{eval_rect, get_parent_rect, read_rect, DrawUpdate, OnModify, Stoppable};
+
+pub type TextPtr = Arc<Text>;
+
+pub struct Text {
+    sg: SceneGraphPtr2,
+    render_api: RenderApiPtr,
+    text_shaper: TextShaperPtr,
+    tasks: Vec<smol::Task<()>>,
+
+    glyphs: Mutex<(Vec<Glyph>, RenderedAtlas)>,
+
+    vertex_buffer: miniquad::BufferId,
+    index_buffer: miniquad::BufferId,
+    num_elements: i32,
+    dc_key: u64,
+
+    node_id: SceneNodeId,
+    rect: PropertyPtr,
+    z_index: PropertyUint32,
+}
+
+impl Text {
+    pub async fn new(
+        ex: Arc<smol::Executor<'static>>,
+        sg: SceneGraphPtr2,
+        node_id: SceneNodeId,
+        render_api: RenderApiPtr,
+        text_shaper: TextShaperPtr,
+    ) -> Pimpl {
+        let scene_graph = sg.lock().await;
+        let node = scene_graph.get_node(node_id).unwrap();
+        let node_name = node.name.clone();
+        let rect = node.get_property("rect").expect("Text::rect");
+        let z_index_prop = node.get_property("z_index").expect("Text::z_index");
+        let z_index = PropertyUint32::from(z_index_prop.clone(), 0).unwrap();
+        let text = node.get_property("text").expect("Text::text");
+        let font_size = node.get_property("font_size").expect("Text::font_size");
+        let color = node.get_property("color").expect("Text::color");
+        let debug = node.get_property("debug").expect("Text::debug");
+        let baseline = node.get_property("baseline").expect("Text::baseline");
+        drop(scene_graph);
+
+        let text_str = text.get_str(0).unwrap();
+        let font_size_val = font_size.get_f32(0).unwrap();
+        debug!(target: "ui::text", "Rendering label '{}'", text_str);
+        let glyphs = text_shaper.shape(text_str, font_size_val).await;
+        let atlas =
+            text2::make_texture_atlas(render_api.clone(), font_size_val, &glyphs).await.unwrap();
+
+        let (x1, y1) = (0., 0.);
+        let (x2, y2) = (1., 1.);
+        let verts = vec![
+            // top left
+            Vertex { pos: [x1, y1], color: [1., 0., 0., 1.], uv: [0., 0.] },
+            // top right
+            Vertex { pos: [x2, y1], color: [1., 0., 1., 1.], uv: [1., 0.] },
+            // bottom left
+            Vertex { pos: [x1, y2], color: [0., 0., 1., 1.], uv: [0., 1.] },
+            // bottom right
+            Vertex { pos: [x2, y2], color: [1., 1., 0., 1.], uv: [1., 1.] },
+        ];
+        let indices = vec![0, 2, 1, 1, 2, 3];
+
+        let num_elements = indices.len() as i32;
+        let vertex_buffer = render_api.new_vertex_buffer(verts).await.unwrap();
+        let index_buffer = render_api.new_index_buffer(indices).await.unwrap();
+
+        let self_ = Arc::new_cyclic(|me: &Weak<Self>| {
+            let mut on_modify = OnModify::new(ex, node_name, node_id, me.clone());
+            on_modify.when_change(rect.clone(), Self::redraw);
+            on_modify.when_change(z_index_prop, Self::redraw);
+            on_modify.when_change(text, Self::redraw);
+            on_modify.when_change(font_size, Self::redraw);
+            on_modify.when_change(color, Self::redraw);
+            on_modify.when_change(debug, Self::redraw);
+            on_modify.when_change(baseline, Self::redraw);
+
+            Self {
+                sg,
+                render_api,
+                text_shaper,
+                tasks: on_modify.tasks,
+                glyphs: Mutex::new((glyphs, atlas)),
+                vertex_buffer,
+                index_buffer,
+                num_elements,
+                dc_key: OsRng.gen(),
+                node_id,
+                rect,
+                z_index,
+            }
+        });
+
+        Pimpl::Text(self_)
+    }
+
+    async fn redraw(self: Arc<Self>) {
+        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;
+        };
+
+        let Some(draw_update) = self.draw(&sg, &parent_rect).await else {
+            error!(target: "ui::text", "Text {:?} failed to draw", node);
+            return;
+        };
+        self.render_api.replace_draw_calls(draw_update.draw_calls).await;
+        debug!(target: "ui::text", "replace draw calls done");
+    }
+
+    pub async fn draw(&self, sg: &SceneGraph, parent_rect: &Rectangle) -> Option<DrawUpdate> {
+        debug!(target: "ui::text", "Text::draw()");
+        // Only used for debug messages
+        let node = sg.get_node(self.node_id).unwrap();
+
+        let mesh = DrawMesh {
+            vertex_buffer: self.vertex_buffer,
+            index_buffer: self.index_buffer,
+            texture: Some(self.glyphs.lock().await.1.texture_id),
+            num_elements: self.num_elements,
+        };
+
+        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.x;
+
+        let off_x = rect.x / parent_rect.w;
+        let off_y = rect.y / parent_rect.h;
+        let scale_x = rect.w / parent_rect.w;
+        let scale_y = rect.h / 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.));
+
+        Some(DrawUpdate {
+            key: self.dc_key,
+            draw_calls: vec![(
+                self.dc_key,
+                DrawCall {
+                    instrs: vec![DrawInstruction::ApplyMatrix(model), DrawInstruction::Draw(mesh)],
+                    dcs: vec![],
+                    z_index: self.z_index.get(),
+                },
+            )],
+        })
+    }
+}
+
+impl Stoppable for Text {
+    async fn stop(&self) {
+        // TODO: Delete own draw call
+
+        // Free buffers
+        // Should this be in drop?
+        //self.render_api.delete_buffer(self.vertex_buffer);
+        //self.render_api.delete_buffer(self.index_buffer);
+    }
+}

+ 9 - 9
bin/darkwallet/src/ui/win.rs

@@ -27,7 +27,7 @@ impl Window {
         render_api: RenderApiPtr,
         render_api: RenderApiPtr,
         event_pub: GraphicsEventPublisherPtr,
         event_pub: GraphicsEventPublisherPtr,
     ) -> Pimpl {
     ) -> Pimpl {
-        debug!(target: "app", "Window::new()");
+        debug!(target: "ui::win", "Window::new()");
 
 
         let scene_graph = sg.lock().await;
         let scene_graph = sg.lock().await;
         let node = scene_graph.get_node(node_id).unwrap();
         let node = scene_graph.get_node(node_id).unwrap();
@@ -46,11 +46,11 @@ impl Window {
             let resize_task = ex.spawn(async move {
             let resize_task = ex.spawn(async move {
                 loop {
                 loop {
                     let Ok((w, h)) = ev_sub.receive().await else {
                     let Ok((w, h)) = ev_sub.receive().await else {
-                        debug!(target: "app", "Event relayer closed");
+                        debug!(target: "ui::win", "Event relayer closed");
                         break
                         break
                     };
                     };
 
 
-                    debug!(target: "app", "Window resized ({w}, {h})");
+                    debug!(target: "ui::win", "Window resized ({w}, {h})");
                     // Now update the properties
                     // Now update the properties
                     screen_size_prop2.set_f32(0, w).unwrap();
                     screen_size_prop2.set_f32(0, w).unwrap();
                     screen_size_prop2.set_f32(1, h).unwrap();
                     screen_size_prop2.set_f32(1, h).unwrap();
@@ -87,7 +87,7 @@ impl Window {
     }
     }
 
 
     pub async fn draw(&self, sg: &SceneGraph) {
     pub async fn draw(&self, sg: &SceneGraph) {
-        debug!(target: "app", "Window::draw()");
+        debug!(target: "ui::win", "Window::draw()");
         // SceneGraph should remain locked for the entire draw
         // SceneGraph should remain locked for the entire draw
         let self_node = sg.get_node(self.node_id).unwrap();
         let self_node = sg.get_node(self.node_id).unwrap();
 
 
@@ -100,12 +100,12 @@ impl Window {
         let mut child_calls = vec![];
         let mut child_calls = vec![];
         for child_inf in self_node.get_children2() {
         for child_inf in self_node.get_children2() {
             let node = sg.get_node(child_inf.id).unwrap();
             let node = sg.get_node(child_inf.id).unwrap();
-            debug!(target: "app", "Window::draw() calling draw() for node '{}':{}", node.name, node.id);
+            debug!(target: "ui::win", "Window::draw() calling draw() for node '{}':{}", node.name, node.id);
 
 
             let dcs = match &node.pimpl {
             let dcs = match &node.pimpl {
-                Pimpl::RenderLayer(layer) => layer.draw(sg, &parent_rect),
+                Pimpl::RenderLayer(layer) => layer.draw(sg, &parent_rect).await,
                 _ => {
                 _ => {
-                    error!(target: "app", "unhandled pimpl type");
+                    error!(target: "ui::win", "unhandled pimpl type");
                     continue
                     continue
                 }
                 }
             };
             };
@@ -116,10 +116,10 @@ impl Window {
 
 
         let root_dc = DrawCall { instrs: vec![], dcs: child_calls, z_index: 0 };
         let root_dc = DrawCall { instrs: vec![], dcs: child_calls, z_index: 0 };
         draw_calls.push((0, root_dc));
         draw_calls.push((0, root_dc));
-        //debug!("  => {:?}", draw_calls);
+        //debug!(target: "ui::win", "  => {:?}", draw_calls);
 
 
         self.render_api.replace_draw_calls(draw_calls).await;
         self.render_api.replace_draw_calls(draw_calls).await;
-        debug!("Window::draw() - replaced draw call");
+        debug!(target: "ui::win", "Window::draw() - replaced draw call");
     }
     }
 }
 }
 
 

+ 42 - 0
bin/darkwallet/src/util.rs

@@ -0,0 +1,42 @@
+use colored::Colorize;
+
+pub fn ansi_texture(width: usize, height: usize, data: &Vec<u8>) -> String {
+    let mut out = String::new();
+
+    out.push('┌');
+    for j in 0..width {
+        out.push('─');
+    }
+    out.push('┐');
+    out.push('\n');
+
+    for i in 0..height {
+        out.push('│');
+        for j in 0..width {
+            let idx = 4 * (i * width + j);
+
+            let r = (data[idx] as f32) / 255.;
+            let g = (data[idx + 1] as f32) / 255.;
+            let b = (data[idx + 2] as f32) / 255.;
+            let a = (data[idx + 3] as f32) / 255.;
+
+            let r = (a * r * 255.) as u8;
+            let g = (a * g * 255.) as u8;
+            let b = (a * b * 255.) as u8;
+
+            let val = "█".truecolor(r, g, b).to_string();
+            out.push_str(&val);
+        }
+        out.push('│');
+        out.push('\n');
+    }
+
+    out.push('└');
+    for j in 0..width {
+        out.push('─');
+    }
+    out.push('┘');
+    out.push('\n');
+
+    out
+}