Explorar o código

Add rendering indirection layer.

Kestrel hai 6 meses
pai
achega
b9219e87f8

+ 4 - 0
Cargo.toml

@@ -12,6 +12,9 @@ jumprope = "1.1"
 # for window initialization
 winit = { version = "0.30.0", default-features = false, features = ["x11", "wayland", "rwh_06"] }
 
+# for font lookup
+fontdb = { version = "0.21", default-features = false, features = ["fs", "memmap"] }
+
 # for layout 
 euclid = { version = "0.22" }
 
@@ -20,6 +23,7 @@ softbuffer = "0.4.2"
 kahlo = { git = "https://git.flying-kestrel.ca/kestrel/kahlo-rs.git", rev = "69001b5" }
 fontdue = "0.9.0"
 
+
 [dev-dependencies]
 serde_json = { version = "1.0" }
 serde = { version = "1.0", features = ["derive"] }

+ 23 - 8
examples/form_demo.rs

@@ -13,13 +13,29 @@ struct FormWindow {
 impl FormWindow {
     fn new(uih: &mut UIHandle) -> Self {
         let mut group = widget::FormGroup::new(uih);
-        group.layout_node_mut().set_height_policy(SizePolicy::fixed(0));
+        group
+            .layout_node_mut()
+            .set_height_policy(SizePolicy::fixed(0));
         group.set_label_margin(PixelSideOffsets::new(0, 10, 0, 10));
-        group.add_widget("button", widget::Button::new_with_label(uih, "engage").boxed());
-        group.add_widget("button 2", widget::Button::new_with_label(uih, "engage").boxed());
-        group.add_widget("button 3", widget::Button::new_with_label(uih, "engage").boxed());
-        group.add_widget("button 4 with longer description", widget::Button::new_with_label(uih, "engage").boxed());
-        Self { root: widget::Frame::wrap_widget(group) }
+        group.add_widget(
+            "button",
+            widget::Button::new(uih).with_label("engage").boxed(),
+        );
+        group.add_widget(
+            "button 2",
+            widget::Button::new(uih).with_label("engage").boxed(),
+        );
+        group.add_widget(
+            "button 3",
+            widget::Button::new(uih).with_label("engage").boxed(),
+        );
+        group.add_widget(
+            "button 4 with longer description",
+            widget::Button::new(uih).with_label("engage").boxed(),
+        );
+        Self {
+            root: widget::Frame::wrap_widget(group),
+        }
     }
 }
 
@@ -30,8 +46,7 @@ impl Component for FormWindow {
     fn process(&mut self, _msg: Self::Msg) -> Vec<Self::ParentMsg> {
         let mut out = String::new();
         patina::layout::dump_node_tree(self.root.layout_node(), &mut out);
-        std::fs::write("patina-form-example.layout_tree", out)
-            .expect("couldn't dump node tree");
+        std::fs::write("patina-form-example.layout_tree", out).expect("couldn't dump node tree");
         vec![UIControlMsg::Terminate]
     }
 }

+ 22 - 11
examples/render_layout_tree.rs

@@ -1,6 +1,5 @@
 use std::collections::HashMap;
 
-
 #[derive(serde::Deserialize)]
 struct Margin {
     top: isize,
@@ -30,19 +29,19 @@ fn main() {
     let execname = args.next().unwrap();
     if args.len() != 1 {
         println!("usage: {} path-to-json", execname);
-        return
+        return;
     }
 
     let fname = args.next().unwrap();
     let indata = std::fs::read(fname).expect("couldn't open file");
 
-    let nodetree : Node = serde_json::from_slice(&indata).expect("couldn't parse file");
+    let nodetree: Node = serde_json::from_slice(&indata).expect("couldn't parse file");
 
-    let mut x_pos : HashMap<usize, usize> = HashMap::new();
-    let mut y_pos : HashMap<usize, usize> = HashMap::new();
+    let mut x_pos: HashMap<usize, usize> = HashMap::new();
+    let mut y_pos: HashMap<usize, usize> = HashMap::new();
 
-    let mut parents : HashMap<usize, usize> = HashMap::new();
-    let mut siblings : HashMap<usize, usize> = HashMap::new();
+    let mut parents: HashMap<usize, usize> = HashMap::new();
+    let mut siblings: HashMap<usize, usize> = HashMap::new();
 
     let mut dfs = vec![&nodetree];
     let mut max_x = 0;
@@ -56,17 +55,25 @@ fn main() {
         // always use parent's y pos + 1
 
         let x = match siblings.get(&node.id) {
-            None => parents.get(&node.id).and_then(|p| x_pos.get(p)).cloned().unwrap_or(0),
+            None => parents
+                .get(&node.id)
+                .and_then(|p| x_pos.get(p))
+                .cloned()
+                .unwrap_or(0),
             Some(_sib) => max_x + 1,
         };
-        let y = parents.get(&node.id).and_then(|p| y_pos.get(p)).map(|v| v + 1).unwrap_or(0);
+        let y = parents
+            .get(&node.id)
+            .and_then(|p| y_pos.get(p))
+            .map(|v| v + 1)
+            .unwrap_or(0);
 
         max_x = max_x.max(x);
 
         x_pos.insert(node.id, x);
         y_pos.insert(node.id, y);
 
-        let mut sibling : Option<usize> = None;
+        let mut sibling: Option<usize> = None;
         for ch in node.children.iter() {
             parents.insert(ch.id, node.id);
             if let Some(sibling) = sibling {
@@ -81,7 +88,11 @@ fn main() {
     yvalues.sort();
     yvalues.dedup();
     for y in yvalues {
-        let mut line = y_pos.iter().filter(|(_,v)| *v == y).map(|(k,_)| (x_pos.get(k).unwrap(), k)).collect::<Vec<_>>();
+        let mut line = y_pos
+            .iter()
+            .filter(|(_, v)| *v == y)
+            .map(|(k, _)| (x_pos.get(k).unwrap(), k))
+            .collect::<Vec<_>>();
         line.sort();
         println!("{line:?}");
     }

+ 14 - 5
examples/text_edits.rs

@@ -17,7 +17,9 @@ struct TextEditWindow {
 impl TextEditWindow {
     fn new(uih: &mut UIHandle) -> Self {
         let mut group = widget::FormGroup::new(uih);
-        group.layout_node_mut().set_height_policy(SizePolicy::fixed(0));
+        group
+            .layout_node_mut()
+            .set_height_policy(SizePolicy::fixed(0));
         group.set_label_margin(PixelSideOffsets::new(0, 10, 0, 10));
 
         group.add_widget("simple editor:", {
@@ -36,7 +38,9 @@ impl TextEditWindow {
             edit.boxed()
         });
 
-        Self { root: widget::Frame::wrap_widget(group) }
+        Self {
+            root: widget::Frame::wrap_widget(group),
+        }
     }
 }
 
@@ -47,11 +51,14 @@ impl Component for TextEditWindow {
     fn process(&mut self, _msg: Self::Msg) -> Vec<Self::ParentMsg> {
         let mut out = String::new();
         patina::layout::dump_node_tree(self.root.layout_node(), &mut out);
-        std::fs::write("patina-form-example.layout_tree", out)
-            .expect("couldn't dump node tree");
+        std::fs::write("patina-form-example.layout_tree", out).expect("couldn't dump node tree");
 
         let jsout = std::fs::File::create("patina-text-edit-example.json").unwrap();
-        patina::layout::dump_tree_json(self.root.layout_node(), &mut std::io::BufWriter::new(jsout)).expect("couldn't dump node tree JSON");
+        patina::layout::dump_tree_json(
+            self.root.layout_node(),
+            &mut std::io::BufWriter::new(jsout),
+        )
+        .expect("couldn't dump node tree JSON");
         vec![UIControlMsg::Terminate]
     }
 }
@@ -76,3 +83,5 @@ impl WindowComponent for TextEditWindow {
 fn main() {
     patina::ui::make_opinionated_ui(TextEditWindow::new).run();
 }
+
+fn main() {}

+ 83 - 0
src/font.rs

@@ -0,0 +1,83 @@
+use std::{cell::RefCell, collections::HashMap, rc::Rc};
+
+/// re-export fontdb
+pub use fontdb;
+
+pub struct FontStore {
+    fontdb: fontdb::Database,
+    loaded: RefCell<HashMap<(fontdb::ID, u32), Rc<fontdue::Font>>>,
+}
+
+impl FontStore {
+    pub fn new_with_system_fonts() -> Self {
+        let mut fontdb = fontdb::Database::new();
+        fontdb.load_system_fonts();
+
+        // query for a sans-serif font from a preselected list
+        let sans_serif_result = fontdb
+            .query(&fontdb::Query {
+                families: &[
+                    fontdb::Family::Name("DejaVu Sans"),
+                    fontdb::Family::Name("Liberation Sans"),
+                    fontdb::Family::Name("FreeSans"),
+                    fontdb::Family::Name("OpenSans"),
+                    fontdb::Family::Name("Arial"),
+                    fontdb::Family::SansSerif,
+                ],
+                ..Default::default()
+            })
+            .expect("Couldn't find any usable fonts on the system");
+
+        // use the resulting font family for a sans-serif font family
+        let finfo = fontdb
+            .face(sans_serif_result)
+            .expect("couldn't load information for font just returned from a query");
+        let family = finfo.families.first().expect("font has no family").clone();
+        fontdb.set_sans_serif_family(family.0);
+
+        Self {
+            fontdb,
+            loaded: Default::default(),
+        }
+    }
+
+    pub fn query_for_font(&self, q: &fontdb::Query) -> Option<Rc<fontdue::Font>> {
+        let qr = self.fontdb.query(q)?;
+        self.fontdb.with_face_data(qr, |data, index| {
+            let mut loaded = self.loaded.borrow_mut();
+            let font = loaded.entry((qr, index)).or_insert_with(|| {
+                Rc::new(
+                    fontdue::Font::from_bytes(
+                        data,
+                        fontdue::FontSettings {
+                            collection_index: index,
+                            ..Default::default()
+                        },
+                    )
+                    .expect("couldn't open font"),
+                )
+            });
+            font.clone()
+        })
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::FontStore;
+    #[test]
+    fn test_font_store() {
+        let fs = FontStore::new_with_system_fonts();
+
+        // query for sans serif font and check that it's nonempty
+        assert!(fs
+            .fontdb
+            .query(&fontdb::Query {
+                families: &[fontdb::Family::SansSerif,],
+                weight: fontdb::Weight::NORMAL,
+                style: fontdb::Style::Normal,
+                ..Default::default()
+            })
+            .is_some());
+    }
+}

+ 10 - 4
src/input.rs

@@ -76,16 +76,22 @@ pub struct InputState {
 
 impl InputState {
     pub fn mouse_status_in(&self, area: PixelBox, button: MouseButton) -> MouseCheckStatus {
-        match (area.contains(self.mouse.pos), area.contains(self.mouse.last_pos)) {
+        match (
+            area.contains(self.mouse.pos),
+            area.contains(self.mouse.last_pos),
+        ) {
             (false, false) => MouseCheckStatus::Idle,
             (true, false) => MouseCheckStatus::Enter,
             (false, true) => MouseCheckStatus::Leave,
-            (true, true) => match (self.mouse.buttons.active(button), self.mouse.last_buttons.active(button)) {
+            (true, true) => match (
+                self.mouse.buttons.active(button),
+                self.mouse.last_buttons.active(button),
+            ) {
                 (false, false) => MouseCheckStatus::Hover,
                 (true, false) => MouseCheckStatus::Click,
                 (false, true) => MouseCheckStatus::Release,
-                (true, true) => MouseCheckStatus::Hold
-            }
+                (true, true) => MouseCheckStatus::Hold,
+            },
         }
     }
 }

+ 57 - 18
src/layout.rs

@@ -15,7 +15,7 @@ mod arr;
 mod cache;
 mod calc;
 
-pub use cache::{LayoutCache,LayoutNodeID};
+pub use cache::{LayoutCache, LayoutNodeID};
 pub use calc::recalculate;
 
 pub struct CellMarker;
@@ -441,37 +441,56 @@ pub fn dump_node_tree(lna: LayoutNodeAccess, out: &mut String) {
     dump_node_tree_helper(lna, 0, out);
 }
 
-pub fn dump_tree_json<W: ?Sized + std::io::Write>(lna: LayoutNodeAccess, out: &mut std::io::BufWriter<W>) -> std::io::Result<()> {
+pub fn dump_tree_json<W: ?Sized + std::io::Write>(
+    lna: LayoutNodeAccess,
+    out: &mut std::io::BufWriter<W>,
+) -> std::io::Result<()> {
     use std::io::Write;
 
     write!(out, "{{")?;
     write!(out, "\"label\": \"{}\" ", lna.label().unwrap_or("null"))?;
-    write!(out, ",\"id\": {} ", <LayoutNodeID as Into<usize>>::into(lna.id()))?;
+    write!(
+        out,
+        ",\"id\": {} ",
+        <LayoutNodeID as Into<usize>>::into(lna.id())
+    )?;
     write!(out, ",\"behaviour\": \"{:?}\"", lna.behaviour())?;
     write!(out, ",\"child_arrangement\": \"{:?}\"", lna.arrangement())?;
-    write!(out, ",\"width_policy\": [{},{},{}]",
-       lna.width_policy().minimum,
-       lna.width_policy().desired,
-       lna.width_policy().slack_weight)?;
-    write!(out, ",\"height_policy\": [{},{},{}]",
-       lna.height_policy().minimum,
-       lna.height_policy().desired,
-       lna.height_policy().slack_weight)?;
+    write!(
+        out,
+        ",\"width_policy\": [{},{},{}]",
+        lna.width_policy().minimum,
+        lna.width_policy().desired,
+        lna.width_policy().slack_weight
+    )?;
+    write!(
+        out,
+        ",\"height_policy\": [{},{},{}]",
+        lna.height_policy().minimum,
+        lna.height_policy().desired,
+        lna.height_policy().slack_weight
+    )?;
     write!(out, ",\"halign\": \"{:?}\"", lna.halign())?;
     write!(out, ",\"valign\": \"{:?}\"", lna.valign())?;
-    write!(out, ",\"margin\": {{\"top\": {}, \"bottom\": {}, \"left\": {}, \"right\": {}}}",
-       lna.margin().top,
-       lna.margin().bottom,
-       lna.margin().left,
-       lna.margin().right,
+    write!(
+        out,
+        ",\"margin\": {{\"top\": {}, \"bottom\": {}, \"left\": {}, \"right\": {}}}",
+        lna.margin().top,
+        lna.margin().bottom,
+        lna.margin().left,
+        lna.margin().right,
     )?;
     match lna.table_cell() {
         None => write!(out, ",\"table_cell\": null")?,
-        Some(c) => write!(out, ",\"table_cell\": [{},{}]", c.x, c.y)?
+        Some(c) => write!(out, ",\"table_cell\": [{},{}]", c.x, c.y)?,
     }
     match lna.render_area() {
         None => write!(out, ",\"render_area\": null")?,
-        Some(a) => write!(out, ",\"render_area\": [[{},{}],[{},{}]]", a.min.x, a.min.y, a.max.x, a.max.y)?
+        Some(a) => write!(
+            out,
+            ",\"render_area\": [[{},{}],[{},{}]]",
+            a.min.x, a.min.y, a.max.x, a.max.y
+        )?,
     }
 
     write!(out, ",\"children\": [")?;
@@ -521,6 +540,26 @@ impl<'l> Iterator for LayoutChildIter<'l> {
 /// Wrapper struct to access a [`LayoutNodeContainer`].
 #[derive(Clone, Copy)]
 pub struct LayoutNodeAccess<'l> {
+    // general problem here:
+    // - this needs to be a reference to make the LNA clone/copy easily,
+    // - the reference means there needs to be a persistent object,
+    // - using the widget as a persistent object means only one level of heirarchy is supported,
+    // - so either:
+    //      a) nodes are stored in some sort of container in the widget, which makes access annoying, or
+    //      b) LayoutNodeContainer is given a tag type parameter to allow multiple implementations on a single type? this somewhat complicates the LayoutNodeAccess type
+    // this could *also* be changed to store a Box<dyn LayoutNodeContainer>, which would allow for a different set of ownership semantics; the result is significantly less efficient as it requires allocations to occur during tree traversal
+    // could require each external node linkage source to return a slice of all child nodes?
+    // - still runs into boxing requirements
+    // could also accept closures that are passed reference to slice of child nodes
+    //
+    // another possible solution here is to simply return an iterator across children
+    // - this does not require an object in quite the same way?
+    // - ExactSizeIterator handles most of the functionality nicely
+    // - and this way there need not be a single trait impl on the widget type; this allows for a
+    // full hierarchy to be specified
+    //
+    // alternatively, simply provide better mechanism for tree storage / easy node access
+    // - simple macro that generates types and impl automatically?
     lnc: &'l dyn LayoutNodeContainer,
 }
 

+ 1 - 1
src/layout/arr.rs

@@ -1,7 +1,7 @@
 use kahlo::math::{PixelBox, PixelSideOffsets};
 use std::ops::Add;
 
-use super::{HorizontalAlignment, LayoutNodeAccess, SizePolicy, VerticalAlignment, SizePolicy2D};
+use super::{HorizontalAlignment, LayoutNodeAccess, SizePolicy, SizePolicy2D, VerticalAlignment};
 
 /// Child arrangement calculator trait.
 ///

+ 2 - 1
src/layout/arr/container.rs

@@ -10,7 +10,8 @@ pub struct ContainerArrangement;
 impl ArrangementCalculator for ContainerArrangement {
     fn arrange_step(&self, node: LayoutNodeAccess) -> SizePolicy2D {
         let Some(child) = node.child(0) else {
-            return SizePolicy2D::new(node.width_policy(), node.height_policy()) + node.margin_as_policy();
+            return SizePolicy2D::new(node.width_policy(), node.height_policy())
+                + node.margin_as_policy();
         };
         SizePolicy2D::new(
             node.width_policy.max_preserve_slack(child.width_policy()),

+ 3 - 2
src/layout/arr/line.rs

@@ -23,7 +23,8 @@ impl LineArrangement {
 impl ArrangementCalculator for LineArrangement {
     fn arrange_step(&self, node: LayoutNodeAccess) -> SizePolicy2D {
         if node.child_len() == 0 {
-            return SizePolicy2D::new(node.width_policy, node.height_policy) + node.margin_as_policy();
+            return SizePolicy2D::new(node.width_policy, node.height_policy)
+                + node.margin_as_policy();
         }
 
         let child_policies = node
@@ -51,7 +52,7 @@ impl ArrangementCalculator for LineArrangement {
             .unwrap();
         SizePolicy2D::new(
             node.width_policy.max_preserve_slack(cw_policy),
-            node.height_policy.max_preserve_slack(ch_policy)
+            node.height_policy.max_preserve_slack(ch_policy),
         ) + node.margin_as_policy()
     }
 

+ 5 - 5
src/layout/arr/table.rs

@@ -1,8 +1,6 @@
 use super::{do_fit, ArrangementCalculator};
 use crate::{
-    layout::{
-        LayoutNodeAccess, SizePolicy, SizePolicy2D,
-    },
+    layout::{LayoutNodeAccess, SizePolicy, SizePolicy2D},
     math::{PixelBox, PixelPoint},
 };
 use std::ops::Add;
@@ -51,7 +49,8 @@ impl ArrangementCalculator for TableArrangement {
 
         // if there are no table cells...
         if tstate.row_policies.len() == 0 || tstate.col_policies.len() == 0 {
-            return SizePolicy2D::new(node.width_policy(), node.height_policy()) + node.margin_as_policy();
+            return SizePolicy2D::new(node.width_policy(), node.height_policy())
+                + node.margin_as_policy();
         }
 
         let net_rows = tstate
@@ -102,7 +101,8 @@ impl ArrangementCalculator for TableArrangement {
                 PixelPoint::new(col_offsets[cell.x + 1], row_offsets[cell.y + 1]),
             );
 
-            ch.child_arrangement.layout_step(ch, cbox.translate(child_offset));
+            ch.child_arrangement
+                .layout_step(ch, cbox.translate(child_offset));
         }
     }
 }

+ 3 - 0
src/lib.rs

@@ -1,4 +1,7 @@
+pub(crate) mod render;
+
 pub mod component;
+pub mod font;
 pub mod input;
 pub mod layout;
 pub mod text;

+ 135 - 0
src/render.rs

@@ -0,0 +1,135 @@
+use std::{cell::{RefCell, Cell}, rc::Rc};
+
+use kahlo::{
+    colour::Colour,
+    math::{PixelBox, PixelSize},
+    prelude::{KahloOps, BitmapAccess},
+};
+
+use crate::{layout::LayoutNode, theme::Theme, window::RenderFormat, ui::UIHandle};
+
+mod text;
+pub use text::TextCache;
+
+#[derive(Default)]
+pub enum ColourChoice {
+    Background,
+    Foreground,
+    Active,
+    Panel,
+    Border,
+    Custom(Colour),
+    #[default]
+    Default,
+}
+
+impl ColourChoice {
+    fn get(self, theme: &Theme, default_to: ColourChoice) -> Colour {
+        match self {
+            Self::Background => theme.background,
+            Self::Foreground => theme.foreground,
+            Self::Active => theme.active,
+            Self::Panel => theme.panel,
+            Self::Border => theme.border,
+            Self::Custom(col) => col,
+            Self::Default => default_to.get(theme, Self::Custom(Colour::RED)),
+        }
+    }
+}
+
+pub struct RenderTarget<'r, 'data: 'r> {
+    theme: &'r Theme,
+    target: &'r mut kahlo::BitmapMut<'data, RenderFormat>,
+}
+
+impl<'a, 'r: 'a, 'data: 'r> RenderTarget<'r, 'data> {
+    pub(crate) fn new(
+        theme: &'r Theme,
+        target: &'r mut kahlo::BitmapMut<'data, RenderFormat>,
+    ) -> Self {
+        Self { theme, target }
+    }
+
+    pub fn with_node(&'a mut self, lnode: &'a LayoutNode) -> NodeRenderTarget<'a, 'r, 'data> {
+        if !lnode.render_check() {
+            return NodeRenderTarget::SkipRender("node does not need to be rerendered");
+        }
+        let Some(area) = lnode.render_area() else {
+            return NodeRenderTarget::SkipRender(
+                "attempting to render node with no render area set",
+            );
+        };
+
+        NodeRenderTarget::Render {
+            rt: self,
+            lnode,
+            area,
+        }
+    }
+}
+
+pub enum NodeRenderTarget<'l, 'r: 'l, 'data: 'r> {
+    Render {
+        rt: &'l mut RenderTarget<'r, 'data>,
+        lnode: &'l LayoutNode,
+        area: PixelBox,
+    },
+    SkipRender(&'static str),
+}
+
+impl<'l, 'r: 'l, 'data: 'r> NodeRenderTarget<'l, 'r, 'data> {
+    pub fn is_skipped(&self) -> bool {
+        match self {
+            Self::Render { .. } => false,
+            Self::SkipRender(_) => true
+        }
+    }
+}
+
+impl<'l, 'r: 'l, 'data: 'r> NodeRenderTarget<'l, 'r, 'data> {
+    pub fn finish(self) {}
+
+
+    pub fn fill(mut self, cc: ColourChoice) -> Self {
+        if let Self::Render { rt, area, .. } = &mut self {
+            rt.target
+                .fill_region(*area, cc.get(rt.theme, ColourChoice::Background));
+        }
+        self
+    }
+
+    pub fn draw_foreground_outline(self) -> Self {
+        self
+    }
+
+    pub fn outline(mut self, width: usize, cc: ColourChoice) -> Self {
+        if let Self::Render { rt, area, .. } = &mut self {
+            rt.target
+                .rectangle(*area, width, cc.get(rt.theme, ColourChoice::Border));
+        }
+        self
+    }
+
+    pub fn border(mut self) -> Self {
+        if let Self::Render { rt, area, .. } = &mut self {
+            rt.target
+                .rectangle(*area, rt.theme.border_width, rt.theme.border);
+        }
+        self
+    }
+
+    pub fn simple_text(mut self, text_cache: &TextCache, cc: ColourChoice) -> Self {
+        if let Self::Render { rt, area, .. } = &mut self {
+            text_cache.with_rendered(|amap| {
+                rt.target.fill_region_masked(
+                    amap,
+                    PixelBox::from_size(amap.size()),
+                    area.min,
+                    cc.get(rt.theme, ColourChoice::Foreground),
+                    kahlo::colour::BlendMode::SourceAlpha,
+                );
+            });
+        }
+        self
+    }
+}

+ 140 - 0
src/render/text.rs

@@ -0,0 +1,140 @@
+use std::{cell::RefCell, rc::Rc};
+
+use kahlo::{
+    math::{PixelBox, PixelSize, PixelPoint},
+    prelude::{KahloOps, BitmapAccess},
+};
+
+use crate::layout::LayoutNode;
+
+pub struct TextCache {
+    text: String,
+    font: Rc<fontdue::Font>,
+    pxsize: f32,
+    rendered: RefCell<Option<kahlo::Alphamap>>,
+}
+
+impl TextCache {
+    pub fn new_with_font_and_size(font: Rc<fontdue::Font>, pxsize: f32) -> Self {
+        Self {
+            text: String::new(),
+            font,
+            pxsize,
+            rendered: Default::default(),
+        }
+    }
+    pub fn text(&self) -> &str {
+        &self.text
+    }
+    pub fn update(&mut self, text: &str) {
+        // if there are actually no changes, ignore
+        if self.text == text {
+            return;
+        }
+        self.text.clear();
+        self.text += text;
+        self.rendered.take();
+    }
+    pub fn change<R, F: Fn(&mut String) -> R>(&mut self, func: F) -> R {
+        let rval = func(&mut self.text);
+        self.rendered.take();
+        rval
+    }
+
+    pub fn size_hint(&self) -> PixelSize {
+        self.calculate_size(self.pxsize, &self.text)
+    }
+
+    pub fn update_node_size_hint(&self, node: &mut LayoutNode) {
+        let sz = self.size_hint();
+
+        let wp = node.width_policy().with_minimum(sz.width as usize);
+        node.set_width_policy(wp);
+        let hp = node.height_policy().with_minimum(sz.height as usize);
+        node.set_height_policy(hp);
+    }
+}
+
+// internal functions
+impl TextCache {
+    pub(crate) fn with_rendered<R>(&self, func: impl FnOnce(&kahlo::Alphamap) -> R) -> Option<R> {
+        let mut rendered = self.rendered.borrow_mut();
+        if let Some(amap) = rendered.as_ref() {
+            return Some(func(amap))
+        }
+
+        *rendered = self.render(self.pxsize);
+
+        rendered.as_ref().map(func)
+    }
+
+    fn render(&self, pxsize: f32) -> Option<kahlo::Alphamap> {
+        // for now assume the input text is a single line
+        let amap = self.render_line(pxsize, &self.text);
+        Some(amap)
+    }
+
+    fn generate_baseline_offsets<'l, 's: 'l>(&'s self, pxsize: f32, text: &'l str) -> impl 'l + Iterator<Item = (u32, u32, char)> {
+        let mut running_offset = 0;
+        let mut last_char = None;
+
+        text.chars().map(move |ch| {
+            let metrics = self.font.metrics(ch, pxsize);
+
+            let glyph_width = if let Some(last) = last_char.take() {
+                metrics.advance_width + self.font.horizontal_kern(last, ch, pxsize).unwrap_or(0.)
+            } else {
+                metrics.advance_width
+            };
+
+            last_char = Some(ch);
+
+            let expose = (running_offset, glyph_width.round() as u32, ch);
+            running_offset += glyph_width.round() as u32;
+            expose
+        })
+    }
+
+    fn calculate_size(&self, pxsize: f32, text: &str) -> PixelSize {
+        // pass: determine required size
+        let total_width = match self.generate_baseline_offsets(pxsize, text).last() {
+            Some(ofs) => ofs.0 + ofs.1,
+            None => 0,
+        };
+        let line_metrics = self.font.horizontal_line_metrics(pxsize).unwrap();
+        let line_height = line_metrics.new_line_size.ceil() as i32;
+
+        PixelSize::new(total_width as i32, line_height)
+    }
+
+    fn render_line(&self, pxsize: f32, text: &str) -> kahlo::Alphamap {
+        // pass 1: determine required size
+        let alphamap_size = self.calculate_size(pxsize, text);
+
+        let line_metrics = self.font.horizontal_line_metrics(pxsize).unwrap();
+        let baseline = line_metrics.ascent.ceil() as usize;
+        let mut alphamap =
+            kahlo::Alphamap::new(alphamap_size.width as usize, alphamap_size.height as usize);
+
+        // pass: generate alphamap
+        for (offset, _width, ch) in self.generate_baseline_offsets(pxsize, text) {
+            let (metrics, raster) = self.font.rasterize(ch, pxsize);
+            let character_alphamap = kahlo::BitmapRef::<kahlo::formats::A8>::new(
+                raster.as_slice(),
+                metrics.width,
+                metrics.height,
+            );
+
+            alphamap.copy_from(
+                &character_alphamap,
+                PixelBox::from_size(character_alphamap.size()),
+                PixelPoint::new(
+                    metrics.xmin + offset as i32,
+                    baseline as i32 - (metrics.height as i32 + metrics.ymin),
+                ),
+            );
+        }
+
+        alphamap
+    }
+}

+ 13 - 3
src/theme.rs

@@ -1,4 +1,9 @@
-use crate::text::TextLine;
+use std::rc::Rc;
+
+use crate::{
+    font::{fontdb as fdb, FontStore},
+    text::TextLine,
+};
 
 use kahlo::colour::Colour;
 
@@ -10,7 +15,7 @@ pub struct Theme {
     pub panel: Colour,
     pub foreground: Colour,
 
-    pub ui_font: fontdue::Font,
+    pub ui_font: Rc<fontdue::Font>,
     pub ui_font_size: f32,
 }
 
@@ -19,7 +24,12 @@ impl Theme {
         TextLine::new(&self.ui_font, text, self.ui_font_size)
     }
 
-    pub fn default_with_font(ui_font: fontdue::Font) -> Self {
+    pub fn build_default(fs: &FontStore) -> Self {
+        let ui_font = fs.query_for_font(&fdb::Query {
+            families: &[fdb::Family::SansSerif],
+            ..Default::default()
+        }).expect("couldn't load sans-serif font!");
+
         Self {
             active: Colour::hex_rgb("#c16e70").unwrap(),
             border: Colour::hex_rgb("#dc9e82").unwrap(),

+ 16 - 17
src/ui.rs

@@ -1,9 +1,10 @@
-use std::collections::HashMap;
+use std::{collections::HashMap, rc::Rc};
 
 use kahlo::math::{PixelPoint, PixelSize};
 
 use crate::{
     component::Component,
+    font::FontStore,
     input::MouseButton,
     layout::{LayoutCache, LayoutNode},
     theme::Theme,
@@ -24,6 +25,8 @@ pub(crate) struct UIState {
     pub(crate) layout_cache: std::rc::Rc<LayoutCache>,
     pub(crate) window_states:
         HashMap<winit::window::WindowId, std::rc::Weak<dyn WindowStateAccess>>,
+
+    pub(crate) fontstore: crate::font::FontStore,
     pub(crate) theme: Theme,
 }
 
@@ -55,7 +58,7 @@ pub struct UI<UIC: UIComponent> {
 }
 
 impl<UIC: UIComponent> UI<UIC> {
-    pub fn new(uic: UIC) -> Self {
+    pub fn new(uic: UIC, fontstore: FontStore) -> Self {
         let lcache = LayoutCache::new();
 
         let eloop = winit::event_loop::EventLoop::builder().build().unwrap();
@@ -65,16 +68,8 @@ impl<UIC: UIComponent> UI<UIC> {
             state: UIState {
                 layout_cache: lcache,
                 window_states: Default::default(),
-                theme: Theme::default_with_font(
-                    fontdue::Font::from_bytes(
-                        std::fs::read(
-                            "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
-                        )
-                        .unwrap(),
-                        Default::default(),
-                    )
-                    .unwrap(),
-                ),
+                theme: Theme::build_default(&fontstore),
+                fontstore,
             },
             event_loop: Some(eloop),
             ui_component: uic,
@@ -192,7 +187,11 @@ impl<UIC: UIComponent> winit::application::ApplicationHandler<()> for UI<UIC> {
                 wsa.notify_resize(PixelSize::new(newsize.width as i32, newsize.height as i32));
                 wsa.request_redraw();
             }
-            winit::event::WindowEvent::KeyboardInput { device_id, event, is_synthetic } => {
+            winit::event::WindowEvent::KeyboardInput {
+                device_id,
+                event,
+                is_synthetic,
+            } => {
                 if event.state == winit::event::ElementState::Pressed {
                     if let Some(text) = event.text {
                         wsa.update_text_input(text.as_str());
@@ -211,8 +210,8 @@ impl<UIC: UIComponent> winit::application::ApplicationHandler<()> for UI<UIC> {
 /// Opinionated base UI component.
 ///
 /// Most of the time, this will be the UI component you will want to use: a single main window,
-/// passing UI events back from the window component. Unless you want multiple windows, there is
-/// no reason to not use this component.
+/// passing UI events back from the window component. Unless you want multiple windows, or need to
+/// use a custom font directory, there is no reason to not use this component.
 pub struct OpinionatedUIComponent<'l, WC: WindowComponent> {
     window: Option<Window<WC>>,
     builder: Option<Box<dyn FnOnce(&mut UIHandle) -> WC + 'l>>,
@@ -249,7 +248,7 @@ impl<'l, WC: WindowComponent<ParentMsg = UIControlMsg>> UIComponent
 }
 
 /// Build an [`OpinionatedUIComponent`] for a given [`WindowComponent`] that passes
-/// [`UIControlMsg`]s back to its parent component.
+/// [`UIControlMsg`]s back to its parent component and uses system fonts.
 pub fn make_opinionated_ui<'l, WC: WindowComponent<ParentMsg = UIControlMsg>>(
     wc: impl 'l + FnOnce(&mut UIHandle) -> WC,
 ) -> UI<OpinionatedUIComponent<'l, WC>> {
@@ -257,5 +256,5 @@ pub fn make_opinionated_ui<'l, WC: WindowComponent<ParentMsg = UIControlMsg>>(
         window: None,
         builder: Some(Box::new(wc)),
     };
-    UI::new(ouic)
+    UI::new(ouic, FontStore::new_with_system_fonts())
 }

+ 3 - 4
src/widget.rs

@@ -2,9 +2,8 @@ use crate::{
     component::Component,
     input::InputState,
     layout::{HorizontalAlignment, LayoutNode, LayoutNodeAccess, TableCell, VerticalAlignment},
-    theme::Theme,
+    render::RenderTarget,
     ui::UIHandle,
-    window::RenderFormat,
 };
 use kahlo::math::PixelSideOffsets;
 
@@ -16,7 +15,7 @@ mod spacer;
 mod text_edit;
 pub use button::Button;
 pub use frame::Frame;
-pub use group::{PlainGroup,FormGroup};
+pub use group::{FormGroup, PlainGroup};
 pub use label::Label;
 pub use spacer::Spacer;
 pub use text_edit::TextEdit;
@@ -28,7 +27,7 @@ pub trait Widget<C: Component> {
 
     fn layout_node(&self) -> LayoutNodeAccess;
     fn layout_node_mut(&mut self) -> &mut LayoutNode;
-    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>);
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>);
 }
 
 pub trait WidgetExt<C: Component>: Widget<C> {

+ 30 - 33
src/widget/button.rs

@@ -1,4 +1,4 @@
-use kahlo::{math::PixelSideOffsets, prelude::*};
+use kahlo::math::PixelSideOffsets;
 
 use crate::{
     component::Component,
@@ -7,11 +7,11 @@ use crate::{
         HorizontalAlignment, LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy,
         VerticalAlignment,
     },
+    render::{ColourChoice, RenderTarget, TextCache},
     ui::UIHandle,
-    window::RenderFormat,
 };
 
-use super::{Label, Widget};
+use super::Widget;
 
 #[derive(Clone, Copy, PartialEq)]
 enum ButtonState {
@@ -22,7 +22,8 @@ enum ButtonState {
 
 pub struct Button<C: Component> {
     layout: LayoutNode,
-    label: Label<C>,
+    text_cache: TextCache,
+    label: String,
     state: ButtonState,
     hook: Option<Box<dyn Fn() -> Option<C::Msg>>>,
 }
@@ -42,20 +43,22 @@ impl<C: Component> Button<C> {
         *layout.margin_mut() = PixelSideOffsets::new_all_same(uih.theme().border_width as i32);
         Self {
             layout,
-            label: Label::new(uih),
+            text_cache: TextCache::new_with_font_and_size(uih.theme().ui_font.clone(), uih.theme().ui_font_size),
+            label: "".to_string(),
             state: ButtonState::Idle,
             hook: None,
         }
     }
 
-    pub fn new_with_label(uih: &UIHandle, label: &str) -> Self {
-        let mut r = Self::new(uih);
-        r.set_label(label);
-        r
+    pub fn with_label(mut self, label: &str) -> Self {
+        self.set_label(label);
+        self
     }
 
     pub fn set_label(&mut self, label: &str) {
-        self.label.set_text(label);
+        self.label.clear();
+        self.label += label;
+        self.text_cache.update(&self.label);
     }
 
     pub fn set_hook(&mut self, to: Box<dyn Fn() -> Option<C::Msg>>) {
@@ -68,14 +71,10 @@ impl<C: Component> LayoutNodeContainer for Button<C> {
         &self.layout
     }
     fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess<'_>> {
-        if ndx == 0 {
-            Some(self.label.layout_node())
-        } else {
-            None
-        }
+        None
     }
     fn layout_child_count(&self) -> usize {
-        1
+        0
     }
 }
 
@@ -91,7 +90,7 @@ impl<C: Component> Widget<C> for Button<C> {
             match istate.mouse_status_in(area, MouseButton::Left) {
                 MouseCheckStatus::Idle | MouseCheckStatus::Leave => {
                     self.state = ButtonState::Idle;
-                },
+                }
                 MouseCheckStatus::Click | MouseCheckStatus::Hold => {
                     self.state = ButtonState::Clicked;
                 }
@@ -107,7 +106,9 @@ impl<C: Component> Widget<C> for Button<C> {
                 self.layout.render_needed();
             }
         }
-        result.extend(self.label.poll(uih, input_state));
+
+        // self.text_cache.update(&mut self.layout, &self.label);
+
         result
     }
 
@@ -118,20 +119,16 @@ impl<C: Component> Widget<C> for Button<C> {
         &mut self.layout
     }
 
-    fn render(&self, theme: &crate::theme::Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {
-        let colour = match self.state {
-            ButtonState::Idle => theme.background,
-            ButtonState::Hovered => theme.panel,
-            ButtonState::Clicked => theme.active,
-        };
-        if self.layout.render_check() {
-            target.fill_region(self.layout.render_area().unwrap(), colour);
-            target.rectangle(
-                self.layout.render_area().unwrap(),
-                theme.border_width,
-                theme.border,
-            );
-        }
-        self.label.render(theme, target);
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>) {
+        target
+            .with_node(&self.layout)
+            .fill(match self.state {
+                ButtonState::Idle => ColourChoice::Background,
+                ButtonState::Hovered => ColourChoice::Panel,
+                ButtonState::Clicked => ColourChoice::Active,
+            })
+            .border()
+            .simple_text(&self.text_cache, ColourChoice::Foreground)
+            .finish();
     }
 }

+ 8 - 10
src/widget/frame.rs

@@ -3,6 +3,7 @@ use kahlo::{math::PixelSideOffsets, prelude::*};
 use crate::{
     component::Component,
     layout::{ChildArrangement, LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy},
+    render::{ColourChoice, RenderTarget},
     theme::Theme,
     ui::UIHandle,
     window::RenderFormat,
@@ -91,18 +92,15 @@ impl<C: Component> Widget<C> for Frame<C> {
     fn layout_node_mut(&mut self) -> &mut LayoutNode {
         &mut self.layout
     }
-    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {
-        let area = self.layout.render_area().unwrap();
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>) {
+        target
+            .with_node(&self.layout)
+            .fill(ColourChoice::Panel)
+            .border()
+            .finish();
 
-        if self.layout.render_check() {
-            target.rectangle(area, theme.border_width, theme.border);
-            target.fill_region(
-                area.inner_box(PixelSideOffsets::new_all_same(theme.border_width as i32)),
-                theme.panel,
-            );
-        }
         if let Some(child) = self.child.as_ref() {
-            child.render(theme, target);
+            child.render(target);
         }
     }
 }

+ 19 - 9
src/widget/group.rs

@@ -1,9 +1,15 @@
+use std::rc::Rc;
+
 use kahlo::math::PixelSideOffsets;
 
 use crate::{
     component::Component,
     input::InputState,
-    layout::{ChildArrangement, LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy, TableCell, HorizontalAlignment},
+    layout::{
+        ChildArrangement, HorizontalAlignment, LayoutNode, LayoutNodeAccess, LayoutNodeContainer,
+        SizePolicy, TableCell,
+    },
+    render::RenderTarget,
     theme::Theme,
     ui::UIHandle,
     widget::Widget,
@@ -82,9 +88,9 @@ impl<C: Component> Widget<C> for PlainGroup<C> {
             .flat_map(|w| w.poll(uih, input_state))
             .collect()
     }
-    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>) {
         for child in self.children.iter() {
-            child.render(theme, target);
+            child.render(target);
         }
     }
 }
@@ -94,6 +100,8 @@ use super::{Label, WidgetExt};
 pub struct FormGroup<C: Component> {
     label_margin: PixelSideOffsets,
     lnode: LayoutNode,
+    label_font: Rc<fontdue::Font>,
+    label_font_size: f32,
     labels: Vec<Label<C>>,
     widgets: Vec<Box<dyn Widget<C>>>,
 }
@@ -105,6 +113,8 @@ impl<C: Component> FormGroup<C> {
         Self {
             label_margin: PixelSideOffsets::new(1, 5, 1, 5),
             lnode,
+            label_font: uih.theme().ui_font.clone(),
+            label_font_size: uih.theme().ui_font_size,
             labels: vec![],
             widgets: vec![],
         }
@@ -125,10 +135,10 @@ impl<C: Component> FormGroup<C> {
         widget.set_table_cell(TableCell::new(1, self.labels.len()));
         self.widgets.push(widget);
         self.labels.push(
-            Label::new_with_node_and_text(self.lnode.make_new_node(), label)
+            Label::new_with_node_and_text(self.label_font.clone(), self.label_font_size, self.lnode.make_new_node(), label)
                 .with_table_cell(TableCell::new(0, self.labels.len()))
                 .with_margins(self.label_margin)
-                .with_halign(HorizontalAlignment::Right)
+                .with_halign(HorizontalAlignment::Right),
         );
     }
 }
@@ -141,7 +151,7 @@ impl<C: Component> LayoutNodeContainer for FormGroup<C> {
         if ndx < self.labels.len() {
             Some(self.labels[ndx].layout_node())
         } else if ndx < (self.labels.len() + self.widgets.len()) {
-            Some(self.widgets[ndx-self.labels.len()].layout_node())
+            Some(self.widgets[ndx - self.labels.len()].layout_node())
         } else {
             None
         }
@@ -176,12 +186,12 @@ impl<C: Component> Widget<C> for FormGroup<C> {
         res
     }
 
-    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>) {
         for lbl in self.labels.iter() {
-            lbl.render(theme, target);
+            lbl.render(target);
         }
         for widget in self.widgets.iter() {
-            widget.render(theme, target);
+            widget.render(target);
         }
     }
 }

+ 27 - 24
src/widget/label.rs

@@ -1,62 +1,58 @@
-use std::cell::RefCell;
+use std::{cell::RefCell, rc::Rc, ops::DerefMut};
 
 use kahlo::{math::PixelBox, prelude::*};
 
 use crate::{
     component::Component,
     layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
-    theme::Theme,
+    render::{RenderTarget, TextCache},
     ui::UIHandle,
-    window::RenderFormat,
 };
 
 use super::Widget;
 
 pub struct Label<C: Component> {
     lnode: LeafLayoutNode,
-    text: Option<String>,
-    rendered: RefCell<Option<kahlo::Alphamap>>,
+    cache: TextCache,
     _ghost: std::marker::PhantomData<C>,
 }
 
 impl<C: Component> Label<C> {
-    fn construct(mut node: LayoutNode, text: Option<&str>) -> Self {
+    fn construct(font: Rc<fontdue::Font>, pxsize: f32, mut node: LayoutNode, text: Option<&str>) -> Self {
         node.set_width_policy(SizePolicy::fixed(0))
             .set_height_policy(SizePolicy::fixed(0));
+        let mut cache = TextCache::new_with_font_and_size(font, pxsize);
+        cache.update(text.unwrap_or(""));
         Self {
             lnode: LeafLayoutNode::new(node),
-            text: text.map(ToString::to_string),
-            rendered: None.into(),
+            cache,
             _ghost: Default::default(),
         }
     }
 
     pub fn new(uih: &UIHandle) -> Self {
-        Self::construct(uih.new_layout_node(), None)
+        Self::construct(uih.theme().ui_font.clone(), uih.theme().ui_font_size, uih.new_layout_node(), None)
     }
 
     pub fn new_with_text(uih: &UIHandle, text: &str) -> Self {
-        Self::construct(uih.new_layout_node(), Some(text))
+        Self::construct(uih.theme().ui_font.clone(), uih.theme().ui_font_size, uih.new_layout_node(), Some(text))
     }
 
-    pub fn new_with_node_and_text(node: LayoutNode, text: &str) -> Self {
-        Self::construct(node, Some(text))
+    pub fn new_with_node_and_text(font: Rc<fontdue::Font>, pxsize: f32, node: LayoutNode, text: &str) -> Self {
+        Self::construct(font, pxsize, node, Some(text))
     }
 
     pub fn set_text(&mut self, text: &str) {
-        if Some(text) != self.text.as_deref() {
-            self.text = Some(text.to_string());
-            self.rendered.take();
-        }
+        self.cache.update(text);
     }
 }
 
 impl<C: Component> Label<C> {
     fn update_hints(&mut self, uih: &UIHandle) {
-        let Some(text) = self.text.as_ref() else {
+        /*let Some(text) = self.text.as_ref() else {
             return;
-        };
-        let line = uih.theme().make_line(text.as_str());
+        };*/
+        /*let line = uih.theme().make_line(text.as_str());
         let (rendered, offset) = line.render_line();
         let sz = rendered.size();
         *self.rendered.borrow_mut() = Some(rendered);
@@ -65,7 +61,7 @@ impl<C: Component> Label<C> {
         let wp = self.lnode.width_policy().with_minimum(sz.width as usize);
         self.lnode.set_width_policy(wp);
         let hp = self.lnode.height_policy().with_minimum(sz.height as usize);
-        self.lnode.set_height_policy(hp);
+        self.lnode.set_height_policy(hp);*/
     }
 }
 
@@ -75,9 +71,11 @@ impl<C: Component> Widget<C> for Label<C> {
         uih: &mut UIHandle,
         _input_state: Option<&crate::input::InputState>,
     ) -> Vec<<C as Component>::Msg> {
-        if self.rendered.borrow().is_none() {
+        // self.cache.size_hint();
+        /*if self.rendered.borrow().is_none() {
             self.update_hints(uih);
-        }
+        }*/
+        self.cache.update_node_size_hint(&mut *self.lnode);
 
         vec![]
     }
@@ -89,8 +87,12 @@ impl<C: Component> Widget<C> for Label<C> {
         &mut self.lnode
     }
 
-    fn render(&self, theme: &Theme, surface: &mut kahlo::BitmapMut<RenderFormat>) {
-        if !self.lnode.render_check() {
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>) {
+
+        target.with_node(&self.lnode)
+            .simple_text(&self.cache, crate::render::ColourChoice::Foreground);
+
+        /*if !self.lnode.render_check() {
             return;
         }
         let rcon = self.rendered.borrow();
@@ -107,5 +109,6 @@ impl<C: Component> Widget<C> for Label<C> {
             theme.foreground,
             kahlo::colour::BlendMode::SourceAlpha,
         );
+        */
     }
 }

+ 8 - 4
src/widget/spacer.rs

@@ -6,10 +6,10 @@ use crate::{
     component::Component,
     input::InputState,
     layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
+    render::RenderTarget,
     theme::Theme,
     ui::UIHandle,
     widget::Widget,
-    window::RenderFormat,
 };
 
 pub struct Spacer<C: Component> {
@@ -46,10 +46,14 @@ impl<C: Component> Widget<C> for Spacer<C> {
         self.lnode.deref_mut()
     }
 
-    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {
-        let region = self.layout_node().render_area().unwrap();
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>) {
+        target
+            .with_node(&self.layout_node())
+            .fill(crate::render::ColourChoice::Background)
+            .finish();
+        /*(let region = self.layout_node().render_area().unwrap();
         if self.lnode.render_check() {
             target.fill_region(region, theme.background);
-        }
+        }*/
     }
 }

+ 42 - 28
src/widget/text_edit.rs

@@ -1,15 +1,19 @@
 use std::cell::RefCell;
 
-use crate::{prelude::*, layout::{LeafLayoutNode, LayoutNodeAccess, LayoutNode, SizePolicy}, ui::UIHandle, input::{MouseButton, MouseCheckStatus}};
-use kahlo::{prelude::*, math::PixelBox};
+use crate::{
+    input::{MouseButton, MouseCheckStatus},
+    layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
+    prelude::*,
+    render::{ColourChoice, RenderTarget, TextCache},
+    ui::UIHandle,
+};
+use kahlo::{math::PixelBox, prelude::*};
 use winit::keyboard::{Key, NamedKey};
 
 pub struct TextEdit<C: Component> {
     lnode: LeafLayoutNode,
-    text: String,
-    text_shift: usize,
+    text: TextCache,
     cursor_pos: Option<usize>,
-    rendered: RefCell<Option<(kahlo::Alphamap, i32)>>,
     onchange: Option<Box<dyn Fn(&str) -> Option<C::Msg>>>,
 }
 
@@ -23,19 +27,17 @@ impl<C: Component> TextEdit<C> {
 
         Self {
             lnode: LeafLayoutNode::new(lnode),
-            text: String::new(),
-            text_shift: 0,
+            text: TextCache::new_with_font_and_size(uih.theme().ui_font.clone(), uih.theme().ui_font_size),
             cursor_pos: None,
-            rendered: None.into(),
-            onchange: None
+            onchange: None,
         }
     }
 
     pub fn set_text(&mut self, to: &str) {
-        self.text = to.to_string();
+        self.text.update(to);
     }
 
-    fn update_hints(&mut self, uih: &UIHandle) {
+    /*fn update_hints(&mut self, uih: &UIHandle) {
         let line = uih.theme().make_line(self.text.as_str());
         let (rendered, offset) = line.render_line();
         let sz = rendered.size();
@@ -46,43 +48,50 @@ impl<C: Component> TextEdit<C> {
         self.lnode.set_width_policy(wp);
         let hp = self.lnode.height_policy().with_minimum(sz.height as usize);
         self.lnode.set_height_policy(hp);
-    }
+    }*/
 }
 
 impl<C: Component> Widget<C> for TextEdit<C> {
-    fn poll(&mut self, uih: &mut UIHandle, input_state: Option<&crate::input::InputState>) -> Vec<<C as Component>::Msg> {
-        let Some(istate) = input_state else { return vec![] };
+    fn poll(
+        &mut self,
+        uih: &mut UIHandle,
+        input_state: Option<&crate::input::InputState>,
+    ) -> Vec<<C as Component>::Msg> {
+        let Some(istate) = input_state else {
+            return vec![];
+        };
 
         if istate.is_focused(self.lnode.id()) {
             if let Some(kp) = &istate.keypress {
                 match kp {
                     Key::Named(NamedKey::Backspace) => {
-                        self.text.pop();
+                        self.text.change(String::pop);
                         self.lnode.render_needed();
-                        self.update_hints(uih);
-                    },
+                        // self.update_hints(uih);
+                    }
                     Key::Named(NamedKey::Delete) => {
-                        self.text.pop();
-                    },
+                        self.text.change(String::pop);
+                        self.lnode.render_needed();
+                    }
                     Key::Named(NamedKey::Enter) => {
                         istate.clear_focus();
-                    },
+                    }
                     _ => {
                         if let Some(inp) = &istate.text_input {
-                            println!("appending text input {inp:x?}");
-                            self.text.push_str(inp.as_str());
+                            self.text.change(|s| s.push_str(inp.as_str()));
                             self.lnode.render_needed();
-                            self.update_hints(uih);
                         }
                     }
                 }
             }
         }
-        if istate.mouse_status_in(self.lnode.render_area().unwrap(), MouseButton::Left) == MouseCheckStatus::Click {
+        if istate.mouse_status_in(self.lnode.render_area().unwrap(), MouseButton::Left)
+            == MouseCheckStatus::Click
+        {
             istate.focus_on(self.lnode.id());
         }
 
-        self.update_hints(uih);
+        // self.update_hints(uih);
 
         vec![]
     }
@@ -94,8 +103,13 @@ impl<C: Component> Widget<C> for TextEdit<C> {
         &mut self.lnode
     }
 
-    fn render(&self, theme: &crate::theme::Theme, target: &mut kahlo::BitmapMut<crate::window::RenderFormat>) {
-        if self.lnode.render_check() {
+    fn render<'r, 'data: 'r>(&self, target: &mut RenderTarget<'r, 'data>) {
+        target
+            .with_node(&self.lnode)
+            .fill(ColourChoice::Background)
+            .simple_text(&self.text, ColourChoice::Foreground);
+
+        /*if self.lnode.render_check() {
             let area = self.lnode.render_area().unwrap();
             target.fill_region(area, theme.background);
 
@@ -105,6 +119,6 @@ impl<C: Component> Widget<C> for TextEdit<C> {
             };
 
             target.fill_region_masked(amap, PixelBox::from_size(amap.size()), area.min, theme.foreground, kahlo::colour::BlendMode::SourceAlpha);
-        }
+        }*/
     }
 }

+ 9 - 5
src/window.rs

@@ -6,10 +6,13 @@ use kahlo::{
     prelude::*,
 };
 
-use crate::input::{InputState, MouseButton};
 use crate::layout::{self, LayoutNode, LayoutNodeAccess, LinearAccess};
 use crate::widget::Widget;
 use crate::{component::Component, ui::UIHandle};
+use crate::{
+    input::{InputState, MouseButton},
+    render::RenderTarget,
+};
 
 pub type RenderFormat = kahlo::formats::Bgr32;
 
@@ -84,9 +87,10 @@ impl<WC: WindowComponent> WindowState<WC> {
             crate::layout::dump_node_tree(LayoutNodeAccess::new(&layout), &mut pre_render_dump);
         }
 
-        self.wc
-            .root_widget()
-            .render(uih.theme(), &mut bitmap.as_mut());
+        let mut bmap = bitmap.as_mut();
+        let mut render_target = RenderTarget::new(uih.theme(), &mut bmap);
+
+        self.wc.root_widget().render(&mut render_target);
         windowbuffer.copy_from(
             bitmap,
             PixelBox::from_size(bitmap.size()),
@@ -100,7 +104,7 @@ impl<WC: WindowComponent> WindowState<WC> {
             "time: {:.03}ms",
             (after - before).as_micros() as f32 / 1000.0
         );
-        let (line,_) = uih.theme().make_line(render_time.as_str()).render_line();
+        let (line, _) = uih.theme().make_line(render_time.as_str()).render_line();
 
         windowbuffer.fill_region_masked(
             &line,