فهرست منبع

wallet: create editbox widget and textshaper tool

darkfi 2 سال پیش
والد
کامیت
7c6b98e43f

+ 116 - 12
bin/darkwallet/gui/__init__.py

@@ -1,4 +1,4 @@
-from pydrk import SceneNodeType, PropertyType, vertex, face
+from pydrk import SceneNodeType, PropertyType, vertex, face, serial
 from .print_tree import print_tree
 from .api import *
 from .gfx import Layer, add_object
@@ -331,10 +331,10 @@ def draw():
     #w, h = 0.1, 0.1
     x, y, w, h = 0, 0, 1, 1
     #x, y, w, h = -1, 1, 2, -2
-    vert1 = vertex(x,     y,     1, 0, 0, 1, 0, 0)
-    vert2 = vertex(x + w, y,     0, 1, 0, 1, 1, 0)
-    vert3 = vertex(x,     y + h, 0, 0, 1, 1, 0, 1)
-    vert4 = vertex(x + w, y + h, 1, 1, 1, 1, 1, 1)
+    vert1 = vertex(x,     y,     0, 0, 0, 1, 0, 0)
+    vert2 = vertex(x + w, y,     0, 0, 0, 1, 1, 0)
+    vert3 = vertex(x,     y + h, 0, 0, 0, 1, 0, 1)
+    vert4 = vertex(x + w, y + h, 0, 0, 0, 1, 1, 1)
 
     verts = vert1 + vert2 + vert3 + vert4
     faces = face(0, 2, 1) + face(1, 2, 3)
@@ -434,6 +434,15 @@ def draw():
     api.set_property_f32(node_id, "rect", 2, 60)
     api.set_property_f32(node_id, "rect", 3, 60)
 
+    prop = Property(
+        "baseline", PropertyType.FLOAT32, PropertySubType.PIXEL,
+        None,
+        "Baseline", "Y offset of baseline inside rect",
+        False, True, 1, None, None, []
+    )
+    api.add_property(node_id, prop)
+    api.set_property_f32(node_id, "baseline", 0, 50)
+
     prop = Property(
         "font_size", PropertyType.FLOAT32, PropertySubType.PIXEL,
         None,
@@ -464,17 +473,106 @@ def draw():
     api.set_property_f32(node_id, "color", 2, 1)
     api.set_property_f32(node_id, "color", 3, 1)
 
+    #prop = Property(
+    #    "overflow", PropertyType.ENUM, PropertySubType.NULL,
+    #    None,
+    #    "Overflow Behaviour", "Behaviour when text exceeds bounding box",
+    #    False, True, 1, None, None, [
+    #        "ScrollRight",
+    #        "OverflowRight"
+    #    ]
+    #)
+    #api.add_property(node_id, prop)
+    #api.set_property_enum(node_id, "overflow", 0, "ScrollRight")
+
+    prop = Property(
+        "z_index", PropertyType.UINT32, PropertySubType.NULL,
+        None,
+        "Z-index", "Z-index: values greater than zero are deferred draws",
+        False, False, 1, None, None, []
+    )
+    api.add_property(node_id, prop)
+
+    prop = Property(
+        "debug", PropertyType.BOOL, PropertySubType.NULL,
+        None,
+        "Debug", "Draw debug outlines",
+        False, False, 1, None, None, []
+    )
+    api.add_property(node_id, prop)
+
+    api.link_node(node_id, layer_id)
+
+    # EditBox
+
+    node_id = api.add_node("editz", SceneNodeType.EDIT_BOX)
+
+    prop = Property(
+        "rect", PropertyType.FLOAT32, PropertySubType.PIXEL,
+        None,
+        "Rectangle", "The position and size within the layer",
+        False, True, 4, None, None, []
+    )
+    api.add_property(node_id, prop)
+    api.set_property_f32(node_id, "rect", 0, 10)
+    api.set_property_f32(node_id, "rect", 1, 200)
+    api.set_property_f32(node_id, "rect", 2, 300)
+    api.set_property_f32(node_id, "rect", 3, 60)
+
     prop = Property(
-        "overflow", PropertyType.ENUM, PropertySubType.NULL,
+        "baseline", PropertyType.FLOAT32, PropertySubType.PIXEL,
         None,
-        "Overflow Behaviour", "Behaviour when text exceeds bounding box",
-        False, True, 1, None, None, [
-            "ScrollRight",
-            "OverflowRight"
-        ]
+        "Baseline", "Y offset of baseline inside rect",
+        False, True, 1, None, None, []
     )
     api.add_property(node_id, prop)
-    api.set_property_enum(node_id, "overflow", 0, "ScrollRight")
+    api.set_property_f32(node_id, "baseline", 0, 50)
+
+    prop = Property(
+        "scroll", PropertyType.FLOAT32, PropertySubType.NULL,
+        None,
+        "Scroll", "Current scroll",
+        False, False, 1, None, None, []
+    )
+    api.add_property(node_id, prop)
+
+    prop = Property(
+        "cursor_pos", PropertyType.UINT32, PropertySubType.NULL,
+        None,
+        "Cursor Position", "Cursor position within the text",
+        False, False, 1, None, None, []
+    )
+    api.add_property(node_id, prop)
+
+    prop = Property(
+        "font_size", PropertyType.FLOAT32, PropertySubType.PIXEL,
+        None,
+        "Font Size", "Font Size",
+        False, True, 1, 0.0, None, []
+    )
+    api.add_property(node_id, prop)
+    api.set_property_f32(node_id, "font_size", 0, 50)
+
+    prop = Property(
+        "text", PropertyType.STR, PropertySubType.NULL,
+        None,
+        "Text", "Text",
+        False, False, 1, None, None, []
+    )
+    api.add_property(node_id, prop)
+    api.set_property_str(node_id, "text", 0, "hello!😁jelly 🍆1234")
+
+    prop = Property(
+        "color", PropertyType.FLOAT32, PropertySubType.COLOR,
+        None,
+        "Color", "Color of the text",
+        False, False, 4, 0, 1, []
+    )
+    api.add_property(node_id, prop)
+    api.set_property_f32(node_id, "color", 0, 1)
+    api.set_property_f32(node_id, "color", 1, 1)
+    api.set_property_f32(node_id, "color", 2, 1)
+    api.set_property_f32(node_id, "color", 3, 1)
 
     prop = Property(
         "z_index", PropertyType.UINT32, PropertySubType.NULL,
@@ -483,6 +581,7 @@ def draw():
         False, False, 1, None, None, []
     )
     api.add_property(node_id, prop)
+    api.set_property_u32(node_id, "z_index", 0, 4)
 
     prop = Property(
         "debug", PropertyType.BOOL, PropertySubType.NULL,
@@ -491,6 +590,11 @@ def draw():
         False, False, 1, None, None, []
     )
     api.add_property(node_id, prop)
+    api.set_property_bool(node_id, "debug", 0, True)
+
+    arg_data = bytearray()
+    serial.write_u32(arg_data, node_id)
+    api.call_method(win_id, "create_edit_box", arg_data)
 
     api.link_node(node_id, layer_id)
 

+ 2 - 2
bin/darkwallet/gui/print_tree.py

@@ -39,8 +39,8 @@ def print_node_info(parent_id, indent):
                 child_type = "fonts"
             case SceneNodeType.FONT:
                 child_type = "font"
-            case SceneNodeType.LINE_POSITION:
-                child_type = "line_position"
+            case SceneNodeType.EDIT_BOX:
+                child_type = "edit_box"
 
         desc = f"{ws}{child_name}:{child_id}/"
         desc += " "*(50 - len(desc))

+ 3 - 1
bin/darkwallet/pydrk/api.py

@@ -56,7 +56,9 @@ class SceneNodeType:
     RENDER_TEXTURE = 13
     FONTS = 10
     FONT = 11
-    LINE_POSITION = 12
+    PLUGINS = 14
+    PLUGIN = 15
+    EDIT_BOX = 16
 
 class PropertyType:
     NULL = 0

+ 156 - 0
bin/darkwallet/src/editbox.rs

@@ -0,0 +1,156 @@
+use miniquad::{KeyMods, UniformType};
+use std::{io::Cursor, sync::Arc};
+use darkfi_serial::Decodable;
+use freetype as ft;
+
+use crate::{error::{Error, Result}, prop::Property, scene::{SceneGraph, SceneNodeId, Pimpl, Slot}, gfx::{Rectangle, RenderContext, COLOR_WHITE, COLOR_BLUE, COLOR_RED, COLOR_GREEN}, text::TextShaper};
+
+pub type EditBoxPtr = Arc<EditBox>;
+
+pub struct EditBox {
+    scroll: Arc<Property>,
+}
+
+impl EditBox {
+    pub fn new(scene_graph: &mut SceneGraph, node_id: SceneNodeId) -> Result<Pimpl> {
+        let node = scene_graph.get_node(node_id).unwrap();
+        let scroll = node.get_property("scroll").ok_or(Error::PropertyNotFound)?;
+
+        println!("EditBox::new()");
+        let self_ = Arc::new(Self{
+            scroll
+        });
+        let weak_self = Arc::downgrade(&self_);
+
+        let slot = Slot {
+            name: "editbox::key_down".to_string(),
+            func: Box::new(move |data| {
+                let mut cur = Cursor::new(&data);
+                let keymods = KeyMods {
+                    shift: Decodable::decode(&mut cur).unwrap(),
+                    ctrl: Decodable::decode(&mut cur).unwrap(),
+                    alt: Decodable::decode(&mut cur).unwrap(),
+                    logo: Decodable::decode(&mut cur).unwrap(),
+                };
+                let repeat = bool::decode(&mut cur).unwrap();
+                let key = String::decode(&mut cur).unwrap();
+
+                let self_ = weak_self.upgrade();
+                if let Some(self_) = self_ {
+                    self_.key_press(key, keymods, repeat);
+                }
+            }),
+        };
+
+        let keyb_node = 
+            scene_graph
+            .lookup_node_mut("/window/input/keyboard")
+            .expect("no keyboard attached!");
+        keyb_node.register("key_down", slot);
+
+        // Save any properties we use
+        Ok(Pimpl::EditBox(self_))
+    }
+
+    pub fn render<'a>(&self, render: &mut RenderContext<'a>, node_id: SceneNodeId, layer_rect: &Rectangle<i32>) -> Result<()> {
+        let node = render.scene_graph.get_node(node_id).unwrap();
+
+        let text = node.get_property_str("text")?;
+        let font_size = node.get_property_f32("font_size")?;
+        let debug = node.get_property_bool("debug")?;
+        let rect = RenderContext::get_dim(node, layer_rect)?;
+        let baseline = node.get_property_f32("baseline")?;
+        let scroll = node.get_property_f32("scroll")?;
+        let cursor_pos = node.get_property_u32("cursor_pos")?;
+
+        let color_prop = node.get_property("color").ok_or(Error::PropertyNotFound)?;
+        let color_r = color_prop.get_f32(0)?;
+        let color_g = color_prop.get_f32(1)?;
+        let color_b = color_prop.get_f32(2)?;
+        let color_a = color_prop.get_f32(3)?;
+        let text_color = [color_r, color_g, color_b, color_a];
+
+        let layer_w = layer_rect.w as f32;
+        let layer_h = layer_rect.h as f32;
+        let off_x = rect.x / layer_w;
+        let off_y = rect.y / layer_h;
+        // Use absolute pixel scale
+        let scale_x = 1. / layer_w;
+        let scale_y = 1. / layer_h;
+        //let model = glam::Mat4::IDENTITY;
+        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 uniforms_data = [0u8; 128];
+        let data: [u8; 64] = unsafe { std::mem::transmute_copy(&render.proj) };
+        uniforms_data[0..64].copy_from_slice(&data);
+        let data: [u8; 64] = unsafe { std::mem::transmute_copy(&model) };
+        uniforms_data[64..].copy_from_slice(&data);
+        assert_eq!(128, 2 * UniformType::Mat4.size());
+
+        render.ctx.apply_uniforms_from_bytes(uniforms_data.as_ptr(), uniforms_data.len());
+
+        let shaper = TextShaper {
+            font_faces: render.font_faces
+        };
+
+        let mut glyph_idx = 0;
+        let mut rhs = 0.;
+
+        for glyph in shaper.shape(text, font_size, text_color) {
+            let texture = render.ctx.new_texture_from_rgba8(glyph.bmp_width, glyph.bmp_height, &glyph.bmp);
+
+            let x1 = glyph.pos.x + scroll;
+            let y1 = glyph.pos.y + baseline;
+            let x2 = x1 + glyph.pos.w;
+            let y2 = y1 + glyph.pos.h;
+
+            let bound = Rectangle {
+                x: 0.,
+                y: 0.,
+                w: rect.w,
+                h: rect.h,
+            };
+            render.render_clipped_box_with_texture(&bound, x1, y1, x2, y2, COLOR_WHITE, texture);
+            //render.render_box_with_texture(x1, y1, x2, y2, COLOR_WHITE, texture);
+            render.ctx.delete_texture(texture);
+
+            if debug {
+                render.outline(x1, y1, x2, y2, COLOR_BLUE, 1.);
+            }
+
+            if cursor_pos == 0 {
+                let cursor_color = [1., 0.5, 0.5, 1.];
+                render.render_box(0., 0., 4., rect.h, cursor_color);
+            }
+            else if cursor_pos == glyph_idx + 1 {
+                let cursor_color = [1., 0.5, 0.5, 1.];
+                render.render_box(x2, 0., x2 + 4., rect.h, cursor_color);
+            }
+
+            glyph_idx += 1;
+
+            rhs = x2;
+        }
+        if debug {
+            render.hline(0., rhs, 0., COLOR_RED, 1.);
+            render.outline(0., 0., rect.w, rect.h, COLOR_GREEN, 1.);
+        }
+
+        Ok(())
+    }
+
+    fn key_press(self: Arc<Self>, key: String, mods: KeyMods, repeat: bool) {
+        if repeat {
+            return;
+        }
+        if key == "PageUp" {
+            println!("pageup!");
+        }
+        else if key == "PageDown" {
+            println!("pagedown!");
+        }
+        // Ability to move cursor
+    }
+}
+

+ 167 - 31
bin/darkwallet/src/gfx.rs

@@ -16,12 +16,13 @@ use std::{
 
 use crate::{
     error::{Error, Result},
+    editbox,
     expr::{SExprMachine, SExprVal},
     prop::{Property, PropertySubType, PropertyType},
     res::{ResourceId, ResourceManager},
     scene::{
         MethodResponseFn, SceneGraph, SceneGraphPtr, SceneNode, SceneNodeId, SceneNodeInfo,
-        SceneNodeType,
+        SceneNodeType, Pimpl
     },
     shader,
 };
@@ -43,9 +44,10 @@ impl MouseButtonAsU8 for MouseButton {
 
 type Color = [f32; 4];
 
-const COLOR_RED: Color = [1., 0., 0., 1.];
-const COLOR_BLUE: Color = [0., 0., 1., 1.];
-const COLOR_WHITE: Color = [1., 1., 1., 1.];
+pub const COLOR_RED: Color = [1., 0., 0., 1.];
+pub const COLOR_GREEN: Color = [0., 1., 0., 1.];
+pub const COLOR_BLUE: Color = [0., 0., 1., 1.];
+pub const COLOR_WHITE: Color = [1., 1., 1., 1.];
 
 #[derive(Debug, SerialEncodable, SerialDecodable)]
 #[repr(C)]
@@ -74,15 +76,15 @@ struct Mesh {
     pub index_buffer: BufferId,
 }
 
-#[derive(Debug)]
-struct Rectangle<T> {
-    x: T,
-    y: T,
-    w: T,
-    h: T,
+#[derive(Debug, Clone)]
+pub struct Rectangle<T: Copy + std::ops::Add<Output=T> + std::ops::Sub<Output=T> + std::cmp::PartialOrd> {
+    pub x: T,
+    pub y: T,
+    pub w: T,
+    pub h: T,
 }
 
-impl<T> Rectangle<T> {
+impl<T: Copy + std::ops::Add<Output=T> + std::ops::Sub<Output=T> + std::cmp::PartialOrd> Rectangle<T> {
     fn from_array(arr: [T; 4]) -> Self {
         let mut iter = IntoIter::new(arr);
         Self {
@@ -92,12 +94,38 @@ impl<T> Rectangle<T> {
             h: iter.next().unwrap(),
         }
     }
+
+    fn clip(&self, other: &Rectangle<T>) -> Option<Rectangle<T>> {
+        if other.x + other.w < self.x ||
+            other.x > self.x + self.w ||
+            other.y + other.h < self.y ||
+            other.y > self.y + self.h
+        {
+            return None
+        }
+
+        let mut clipped = other.clone();
+        if clipped.x < self.x {
+            clipped.x = self.x;
+        }
+        if clipped.y < self.y {
+            clipped.y = self.y;
+        }
+        if clipped.x + clipped.w > self.x + self.w {
+            clipped.w = self.x + self.w - clipped.x;
+        }
+        if clipped.y + clipped.h > self.y + self.h {
+            clipped.h = self.y + self.h - clipped.y;
+        }
+        Some(clipped)
+    }
 }
 
 #[derive(Debug)]
 enum GraphicsMethodEvent {
     LoadTexture,
     DeleteTexture,
+    CreateEditBox,
 }
 
 struct Stage<'a> {
@@ -213,8 +241,10 @@ impl<'a> Stage<'a> {
                 ],
             )
             .unwrap();
-        let sender = self.method_sender.clone();
+
         let window_id = window.id;
+
+        let sender = self.method_sender.clone();
         let method_fn = Box::new(move |arg_data, response_fn| {
             sender.send((GraphicsMethodEvent::LoadTexture, window_id, arg_data, response_fn));
         });
@@ -226,6 +256,20 @@ impl<'a> Stage<'a> {
                 method_fn,
             )
             .unwrap();
+
+        let sender = self.method_sender.clone();
+        let method_fn = Box::new(move |arg_data, response_fn| {
+            sender.send((GraphicsMethodEvent::CreateEditBox, window_id, arg_data, response_fn));
+        });
+        window
+            .add_method(
+                "create_edit_box",
+                vec![("node_id", "", PropertyType::SceneNodeId)],
+                vec![],
+                method_fn,
+            )
+            .unwrap();
+
         scene_graph.link(window_id, SceneGraph::ROOT_ID).unwrap();
 
         let input = scene_graph.add_node("input", SceneNodeType::WindowInput);
@@ -336,15 +380,33 @@ impl<'a> Stage<'a> {
         self.textures.free(texture_id);
         Ok(vec![])
     }
+
+    fn method_create_editbox(
+        &mut self,
+        _: SceneNodeId,
+        arg_data: Vec<u8>,
+    ) -> Result<Vec<u8>> {
+        let mut cur = Cursor::new(&arg_data);
+        let node_id = SceneNodeId::decode(&mut cur).unwrap();
+
+        let mut scene_graph = self.scene_graph.lock().unwrap();
+        let editbox = editbox::EditBox::new(&mut scene_graph, node_id)?;
+
+        let node = scene_graph.get_node_mut(node_id).ok_or(Error::NodeNotFound)?;
+        node.pimpl = editbox;
+
+        let mut reply = vec![];
+        Ok(reply)
+    }
 }
 
-struct RenderContext<'a> {
-    scene_graph: MutexGuard<'a, SceneGraph>,
-    ctx: &'a mut Box<dyn RenderingBackend>,
-    pipeline: &'a Pipeline,
-    proj: glam::Mat4,
-    textures: &'a ResourceManager<TextureId>,
-    font_faces: &'a Vec<ft::Face<&'a [u8]>>,
+pub struct RenderContext<'a> {
+    pub scene_graph: MutexGuard<'a, SceneGraph>,
+    pub ctx: &'a mut Box<dyn RenderingBackend>,
+    pub pipeline: &'a Pipeline,
+    pub proj: glam::Mat4,
+    pub textures: &'a ResourceManager<TextureId>,
+    pub font_faces: &'a Vec<ft::Face<&'a [u8]>>,
 }
 
 impl<'a> RenderContext<'a> {
@@ -415,7 +477,7 @@ impl<'a> RenderContext<'a> {
         self.ctx.apply_scissor_rect(rect.x, rect.y, rect.w, rect.h);
 
         let layer_children =
-            layer.get_children(&[SceneNodeType::RenderMesh, SceneNodeType::RenderText]);
+            layer.get_children(&[SceneNodeType::RenderMesh, SceneNodeType::RenderText, SceneNodeType::EditBox]);
         let layer_children = self.order_by_z_index(layer_children);
 
         // get the rectangle
@@ -442,6 +504,16 @@ impl<'a> RenderContext<'a> {
                         error!("error rendering text '{}': {}", child.name, err);
                     }
                 }
+                SceneNodeType::EditBox => {
+                    let node = self.scene_graph.get_node(child.id).unwrap();
+                    let editbox = match &node.pimpl {
+                        Pimpl::EditBox(e) => e.clone(),
+                        _ => panic!("wrong pimpl for editbox")
+                    };
+                    if let Err(err) = editbox.render(self, child.id, &rect) {
+                        error!("error rendering editbox '{}': {}", child.name, err);
+                    }
+                }
                 _ => panic!("render_layer(): unknown type"),
             }
         }
@@ -462,7 +534,7 @@ impl<'a> RenderContext<'a> {
         nodes.into_iter().map(|(z_index, node_inf)| node_inf).collect()
     }
 
-    fn get_dim(mesh: &SceneNode, layer_rect: &Rectangle<i32>) -> Result<Rectangle<f32>> {
+    pub fn get_dim(mesh: &SceneNode, layer_rect: &Rectangle<i32>) -> Result<Rectangle<f32>> {
         let prop = mesh.get_property("rect").ok_or(Error::PropertyNotFound)?;
         if prop.array_len != 4 {
             return Err(Error::PropertyWrongLen)
@@ -553,7 +625,69 @@ impl<'a> RenderContext<'a> {
         Ok(())
     }
 
-    fn render_box_with_texture(
+    pub fn render_clipped_box_with_texture(
+        &mut self,
+        bound_rect: &Rectangle<f32>,
+        x1: f32,
+        y1: f32,
+        x2: f32,
+        y2: f32,
+        color: Color,
+        texture: TextureId,
+    ) {
+        let obj = Rectangle {
+            x: x1,
+            y: y1,
+            w: x2 - x1,
+            h: y2 - y1
+        };
+        let Some(clipped) = bound_rect.clip(&obj) else {
+            return
+        };
+
+        let x1 = clipped.x;
+        let y1 = clipped.y;
+        let x2 = clipped.x + clipped.w;
+        let y2 = clipped.y + clipped.h;
+
+        let u1 = (clipped.x - obj.x) / obj.w;
+        let u2 = (clipped.x + clipped.w - obj.x) / obj.w + u1;
+        let v1 = (clipped.y - obj.y) / obj.h;
+        let v2 = (clipped.y + clipped.h - obj.y) / obj.h + v1;
+
+        let vertices: [Vertex; 4] = [
+            // top left
+            Vertex { pos: [x1, y1], color, uv: [u1, v1] },
+            // top right
+            Vertex { pos: [x2, y1], color, uv: [u2, v1] },
+            // bottom left
+            Vertex { pos: [x1, y2], color, uv: [u1, v2] },
+            // bottom right
+            Vertex { pos: [x2, y2], color, uv: [u2, v2] },
+        ];
+
+        //debug!("screen size: {:?}", window::screen_size());
+        let vertex_buffer = self.ctx.new_buffer(
+            BufferType::VertexBuffer,
+            BufferUsage::Immutable,
+            BufferSource::slice(&vertices),
+        );
+
+        let indices: [u16; 6] = [0, 2, 1, 1, 2, 3];
+        let index_buffer = self.ctx.new_buffer(
+            BufferType::IndexBuffer,
+            BufferUsage::Immutable,
+            BufferSource::slice(&indices),
+        );
+
+        let bindings =
+            Bindings { vertex_buffers: vec![vertex_buffer], index_buffer, images: vec![texture] };
+
+        self.ctx.apply_bindings(&bindings);
+        self.ctx.draw(0, 6, 1);
+    }
+
+    pub fn render_box_with_texture(
         &mut self,
         x1: f32,
         y1: f32,
@@ -594,20 +728,20 @@ impl<'a> RenderContext<'a> {
         self.ctx.draw(0, 6, 1);
     }
 
-    fn render_box(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color) {
+    pub fn render_box(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color) {
         let texture = self.textures.get(Stage::WHITE_TEXTURE_ID).unwrap();
         self.render_box_with_texture(x1, y1, x2, y2, color, *texture)
     }
 
-    fn hline(&mut self, min_x: f32, max_x: f32, y: f32, color: Color, w: f32) {
+    pub fn hline(&mut self, min_x: f32, max_x: f32, y: f32, color: Color, w: f32) {
         self.render_box(min_x, y - w / 2., max_x, y + w / 2., color)
     }
 
-    fn vline(&mut self, x: f32, min_y: f32, max_y: f32, color: Color, w: f32) {
+    pub fn vline(&mut self, x: f32, min_y: f32, max_y: f32, color: Color, w: f32) {
         self.render_box(x - w / 2., min_y, x + w / 2., max_y, color)
     }
 
-    fn outline(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, w: f32) {
+    pub fn outline(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, w: f32) {
         // top
         self.render_box(x1, y1, x2, y1 + w, color);
         // left
@@ -618,15 +752,14 @@ impl<'a> RenderContext<'a> {
         self.render_box(x1, y2 - w, x2, y2, color);
     }
 
-    fn render_text(&mut self, mesh_id: SceneNodeId, layer_rect: &Rectangle<i32>) -> Result<()> {
-        let node = self.scene_graph.get_node(mesh_id).unwrap();
+    fn render_text(&mut self, node_id: SceneNodeId, layer_rect: &Rectangle<i32>) -> Result<()> {
+        let node = self.scene_graph.get_node(node_id).unwrap();
 
-        let z_index = node.get_property_u32("z_index")?;
         let text = node.get_property_str("text")?;
-        let overflow = node.get_property_enum("overflow")?;
         let font_size = node.get_property_f32("font_size")?;
         let debug = node.get_property_bool("debug")?;
         let rect = Self::get_dim(node, layer_rect)?;
+        let baseline = node.get_property_f32("baseline")?;
 
         let color_prop = node.get_property("color").ok_or(Error::PropertyNotFound)?;
         let color_r = color_prop.get_f32(0)?;
@@ -696,7 +829,7 @@ impl<'a> RenderContext<'a> {
         }
 
         let mut current_x = 0.;
-        let mut current_y = 0.;
+        let mut current_y = baseline;
 
         for (face_idx, text) in substrs {
             let face = &self.font_faces[face_idx];
@@ -717,6 +850,8 @@ impl<'a> RenderContext<'a> {
 
             for (position, info) in positions.iter().zip(infos) {
                 let gid = info.codepoint;
+                // Index within this substr
+                // let cluster = info.cluster;
 
                 let mut flags = ft::face::LoadFlag::DEFAULT;
                 if face.has_color() {
@@ -856,6 +991,7 @@ impl<'a> EventHandler for Stage<'a> {
             let res = match event {
                 GraphicsMethodEvent::LoadTexture => self.method_load_texture(node_id, arg_data),
                 GraphicsMethodEvent::DeleteTexture => self.method_delete_texture(node_id, arg_data),
+                GraphicsMethodEvent::CreateEditBox => self.method_create_editbox(node_id, arg_data),
             };
             response_fn(res);
         }

+ 4 - 0
bin/darkwallet/src/main.rs

@@ -5,6 +5,8 @@ use std::{
     thread,
 };
 
+mod editbox;
+
 mod error;
 
 mod expr;
@@ -28,6 +30,8 @@ mod res;
 
 mod shader;
 
+mod text;
+
 #[macro_use]
 extern crate log;
 #[allow(unused_imports)]

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

@@ -11,6 +11,7 @@ use std::{
 
 use crate::{
     error::{Error, Result},
+    editbox,
     prop::{Property, PropertyType},
 };
 
@@ -32,9 +33,9 @@ pub enum SceneNodeType {
     RenderTexture = 13,
     Fonts = 10,
     Font = 11,
-    LinePosition = 12,
     Plugins = 14,
     Plugin = 15,
+    EditBox = 16,
 }
 
 pub struct ScenePath(Vec<String>);
@@ -107,6 +108,7 @@ impl SceneGraph {
             props: vec![],
             sigs: vec![],
             methods: vec![],
+            pimpl: Pimpl::Null,
         };
         Self { nodes: vec![root], freed: vec![] }
     }
@@ -122,6 +124,7 @@ impl SceneGraph {
             props: vec![],
             sigs: vec![],
             methods: vec![],
+            pimpl: Pimpl::Null,
         };
 
         let node_id = if self.freed.is_empty() {
@@ -356,6 +359,7 @@ pub struct SceneNode {
     pub props: Vec<Arc<Property>>,
     pub sigs: Vec<Signal>,
     pub methods: Vec<Method>,
+    pub pimpl: Pimpl
 }
 
 impl SceneNode {
@@ -622,3 +626,9 @@ pub struct Method {
     pub result: Vec<CallArg>,
     method_fn: MethodRequestFn,
 }
+
+pub enum Pimpl {
+    Null,
+    EditBox(editbox::EditBoxPtr),
+}
+

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

@@ -0,0 +1,188 @@
+use freetype as ft;
+
+use crate::gfx::Rectangle;
+
+pub struct Glyph {
+    // Substring this glyph corresponds to
+    pub substr: String,
+
+    // The texture
+    pub bmp: Vec<u8>,
+    pub bmp_width: u16,
+    pub bmp_height: u16,
+
+    pub pos: Rectangle<f32>,
+}
+
+pub struct TextShaper<'a> {
+    pub font_faces: &'a Vec<ft::Face<&'a [u8]>>,
+}
+
+impl<'a> TextShaper<'a> {
+    fn split_into_substrs(&self, 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..self.font_faces.len() {
+                    let ft_face = &self.font_faces[i];
+                    if ft_face.get_char_index(chr as usize).is_some() {
+                        break 'get_idx i
+                    }
+                }
+
+                warn!("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 fn shape(&self, text: String, font_size: f32, text_color: [f32; 4]) -> Vec<Glyph> {
+        let substrs = self.split_into_substrs(text.clone());
+
+        let mut glyphs = vec![];
+
+        let mut current_x = 0.;
+        let mut current_y = 0.;
+
+        for (face_idx, text) in substrs {
+            let face = &self.font_faces[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().add_str(&text);
+            let output = harfbuzz_rs::shape(&hb_font, buffer, &[]);
+
+            let positions = output.get_glyph_positions();
+            let infos = output.get_glyph_infos();
+
+            for (position, info) in positions.iter().zip(infos) {
+                let gid = info.codepoint;
+                // Index within this substr
+                // let cluster = info.cluster;
+
+                let mut flags = ft::face::LoadFlag::DEFAULT;
+                if face.has_color() {
+                    flags |= ft::face::LoadFlag::COLOR;
+                }
+                face.load_glyph(gid, flags).unwrap();
+
+                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 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. * text_color[0]) as u8;
+                                let g = (255. * text_color[1]) as u8;
+                                let b = (255. * text_color[2]) as u8;
+                                let α = ((*coverage as f32) * text_color[3]) as u8;
+                                vec![r, g, b, α]
+                            })
+                            .collect();
+                        tdata
+                    }
+                    _ => panic!("unsupport pixel mode: {:?}", pixel_mode)
+                };
+
+                let pos = if face.has_fixed_sizes() {
+                    // Downscale by height
+                    let w = (bmp_width as f32 * font_size) / bmp_height as f32;
+                    let h = font_size;
+
+                    let x = current_x;
+                    let y = current_y - h;
+
+                    current_x += w;
+
+                    Rectangle {
+                        x, y, w, h
+                    }
+                } else {
+                    let (w, h) = (bmp_width as f32, bmp_height as f32);
+
+                    let off_x = position.x_offset as f32 / 64.;
+                    let off_y = position.y_offset as f32 / 64.;
+
+                    let x = current_x + off_x + bearing_x;
+                    let y = current_y - off_y - bearing_y;
+
+                    let x_advance = position.x_advance as f32 / 64.;
+                    let y_advance = position.y_advance as f32 / 64.;
+                    current_x += x_advance;
+                    current_y += y_advance;
+
+                    Rectangle {
+                        x, y, w, h
+                    }
+                };
+
+                let substr = "hello".to_string();
+
+                let glyph = Glyph {
+                    substr,
+                    bmp,
+                    bmp_width: bmp_width as u16,
+                    bmp_height: bmp_height as u16,
+                    pos
+                };
+
+                glyphs.push(glyph);
+            }
+        }
+
+        glyphs
+    }
+}
+