2 Komitmen 31fc9a0f28 ... f6581fa7c1

Pembuat SHA1 Pesan Tanggal
  Kestrel f6581fa7c1 rustfmt pass. 3 bulan lalu
  Kestrel d037e38cee Minor changes to streamline examples, start of table layout. 3 bulan lalu
15 mengubah file dengan 461 tambahan dan 195 penghapusan
  1. 1 2
      Cargo.toml
  2. 14 15
      examples/button.rs
  3. 53 0
      examples/table_layout.rs
  4. 14 9
      src/layout.rs
  5. 14 119
      src/layout/arr.rs
  6. 123 0
      src/layout/arr/line.rs
  7. 37 0
      src/layout/arr/table.rs
  8. 20 22
      src/layout/calc.rs
  9. 0 1
      src/theme.rs
  10. 6 3
      src/ui.rs
  11. 12 4
      src/widget/button.rs
  12. 2 1
      src/widget/frame.rs
  13. 77 3
      src/widget/group.rs
  14. 20 7
      src/widget/label.rs
  15. 68 9
      src/window.rs

+ 1 - 2
Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "patina"
-version = "0.1.0"
+version = "0.0.1"
 edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -17,5 +17,4 @@ softbuffer = "0.4.2"
 kahlo = { git = "https://git.flying-kestrel.ca/kestrel/kahlo-rs.git", rev = "69001b5" }
 fontdue = "0.9.0"
 
-
 [dev-dependencies]

+ 14 - 15
examples/button.rs

@@ -1,5 +1,4 @@
 use patina::{
-    layout::SizePolicy,
     prelude::*,
     ui::{UIControlMsg, UIHandle},
     widget,
@@ -15,20 +14,20 @@ struct ButtonWindow {
 
 impl ButtonWindow {
     fn new(uih: &mut UIHandle) -> Self {
-        let mut group = widget::PlainGroup::new(uih);
-        group
-            .layout_node_mut()
-            .set_arrangement(patina::layout::ChildArrangement::Column)
-            .set_width_policy(SizePolicy::expanding(1))
-            .set_height_policy(SizePolicy::expanding(1));
-        group.append(Box::new(widget::Spacer::new(uih)));
-        group.append(Box::new({
-            let mut button = widget::Button::new(uih);
-            button.set_label("Button label");
-            button.set_hook(Box::new(|| Some(ButtonWindowMsg::Shutdown)));
-            button
-        }));
-        group.append(Box::new(widget::Spacer::new(uih)));
+        let mut group = widget::PlainGroup::new_column(uih);
+        group.extend(
+            vec![
+                Box::new(widget::Spacer::new(uih)) as Box<dyn Widget<Self>>,
+                Box::new({
+                    let mut button = widget::Button::new(uih);
+                    button.set_label("Button label");
+                    button.set_hook(Box::new(|| Some(ButtonWindowMsg::Shutdown)));
+                    button
+                }),
+                Box::new(widget::Spacer::new(uih)),
+            ]
+            .into_iter(),
+        );
         Self { group }
     }
 }

+ 53 - 0
examples/table_layout.rs

@@ -0,0 +1,53 @@
+use patina::{
+    prelude::*,
+    ui::{UIControlMsg, UIHandle},
+    widget,
+};
+
+struct TableWindow {
+    root: widget::PlainGroup<Self>,
+}
+
+impl TableWindow {
+    fn new(uih: &mut UIHandle) -> Self {
+        let mut group = widget::PlainGroup::new_table(uih);
+        group.extend(
+            vec![
+                Box::new(widget::Spacer::new(uih)) as Box<dyn Widget<Self>>,
+                Box::new(widget::Spacer::new(uih)),
+            ]
+            .into_iter(),
+        );
+        Self { root: group }
+    }
+}
+
+impl Component for TableWindow {
+    type ParentMsg = UIControlMsg;
+    type Msg = ();
+
+    fn process(&mut self, _msg: Self::Msg) -> Vec<Self::ParentMsg> {
+        vec![UIControlMsg::Terminate]
+    }
+}
+
+impl WindowComponent for TableWindow {
+    fn map_window_event(&self, we: patina::window::WindowEvent) -> Option<Self::Msg> {
+        match we {
+            patina::window::WindowEvent::CloseRequested => Some(()),
+            _ => None,
+        }
+    }
+
+    type RootWidget = widget::PlainGroup<Self>;
+    fn root_widget(&self) -> &Self::RootWidget {
+        &self.root
+    }
+    fn root_widget_mut(&mut self) -> &mut Self::RootWidget {
+        &mut self.root
+    }
+}
+
+fn main() {
+    patina::ui::make_opinionated_ui(TableWindow::new).run();
+}

+ 14 - 9
src/layout.rs

@@ -6,7 +6,7 @@
 //! - **Layout**: given minimum sizes and net size policies from the arrangement step, computes
 //! exact pixel values for positions and sizes.
 
-use std::{ops::Deref, rc::Rc};
+use std::{collections::HashMap, ops::Deref, rc::Rc};
 
 use cache::{Layer, LayoutCacheKey, NodeState};
 use kahlo::math::{PixelBox, PixelSideOffsets};
@@ -152,7 +152,7 @@ impl Deref for ChildArrangement {
             Self::Custom(calc) => calc.as_ref(),
             Self::Column => &arr::LineArrangement::Column,
             Self::Row => &arr::LineArrangement::Row,
-            Self::Table => todo!(),
+            Self::Table => &arr::TableArrangement,
         }
     }
 }
@@ -178,6 +178,8 @@ pub struct LayoutNode {
     valign: VerticalAlignment,
     /// User-exposed margins, or spacing between the parent and the render area of this node.
     margin: PixelSideOffsets,
+    /// Table row/column assignment for child nodes, coordinates stored as (row, column).
+    table_cells: Option<HashMap<(usize, usize), LayoutCacheKey>>,
 }
 
 impl std::fmt::Debug for LayoutNode {
@@ -209,6 +211,7 @@ impl LayoutNode {
             halign: HorizontalAlignment::default(),
             valign: VerticalAlignment::default(),
             margin: PixelSideOffsets::new_all_same(0),
+            table_cells: None,
         }
     }
 
@@ -217,7 +220,7 @@ impl LayoutNode {
     }
 
     pub fn relayout_tree(&self) {
-        let mut to_mark : Vec<LayoutCacheKey> = vec![self.cache_key];
+        let mut to_mark: Vec<LayoutCacheKey> = vec![self.cache_key];
         while let Some(next) = to_mark.pop() {
             self.cache.with_state(next, |ns| {
                 ns.needs_update = true;
@@ -235,11 +238,13 @@ impl LayoutNode {
 
     /// Checks if node needs to be rerendered, clearing the flag if so.
     pub fn render_check(&self) -> bool {
-        self.cache.with_state(self.cache_key, |ns| {
-            let ret = ns.needs_render;
-            ns.needs_render = false;
-            ret
-        }).unwrap_or(false)
+        self.cache
+            .with_state(self.cache_key, |ns| {
+                let ret = ns.needs_render;
+                ns.needs_render = false;
+                ret
+            })
+            .unwrap_or(false)
     }
 
     pub fn render_needed(&self) {
@@ -329,6 +334,7 @@ impl Clone for LayoutNode {
             halign: self.halign,
             valign: self.valign,
             margin: self.margin,
+            table_cells: self.table_cells.clone(),
         }
     }
 }
@@ -372,7 +378,6 @@ fn dump_node_tree_helper(lna: LayoutNodeAccess, indent: usize, out: &mut String)
 }
 
 pub fn dump_node_tree(lna: LayoutNodeAccess, out: &mut String) {
-    out.clear();
     dump_node_tree_helper(lna, 0, out);
 }
 

+ 14 - 119
src/layout/arr.rs

@@ -12,123 +12,10 @@ pub trait ArrangementCalculator {
     fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox);
 }
 
-#[derive(Clone, Debug)]
-pub enum LineArrangement {
-    Column,
-    Row,
-}
-
-impl LineArrangement {
-    fn is_column(&self) -> bool {
-        match self {
-            Self::Column => true,
-            Self::Row => false,
-        }
-    }
-}
-
-impl ArrangementCalculator for LineArrangement {
-    fn arrange_step(
-        &self,
-        node: LayoutNodeAccess,
-        child_policies: Vec<(SizePolicy, SizePolicy)>,
-    ) -> (SizePolicy, SizePolicy) {
-        if child_policies.is_empty() {
-            return (node.width_policy, node.height_policy);
-        }
-
-        let cw_policy = child_policies
-            .iter()
-            .map(|v| v.0)
-            .reduce(if self.is_column() {
-                SizePolicy::max
-            } else {
-                SizePolicy::add
-            })
-            .unwrap();
-        let ch_policy: SizePolicy = child_policies
-            .iter()
-            .map(|v| v.1)
-            .reduce(if self.is_column() {
-                SizePolicy::add
-            } else {
-                SizePolicy::max
-            })
-            .unwrap();
-        (
-            node.width_policy.max_preserve_slack(cw_policy),
-            node.height_policy.max_preserve_slack(ch_policy),
-        )
-    }
-
-    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {
-        // do the final children layouts
-        node.cache
-            .with_state(node.cache_key, |ns| {
-                if Some(inside) != ns.area {
-                    ns.area = Some(inside);
-                    ns.needs_render = true;
-                }
-            });
-
-        if node.child_len() == 0 {
-            return;
-        }
-
-        // expansion direction extraction lambda
-        let ee = if self.is_column() {
-            |ns: &mut NodeState| ns.net_policy.1
-        } else {
-            |ns: &mut NodeState| ns.net_policy.0
-        };
-
-        let policies = node
-            .child_iter()
-            .map(|c| node.cache.with_state(c.cache_key, ee).unwrap());
-        let mut last_offset = 0;
-        let fit = do_fit(
-            if self.is_column() {
-                inside.height() as i32
-            } else {
-                inside.width() as i32
-            },
-            policies,
-        );
-
-        for (offset, child) in fit.zip(node.child_iter()) {
-            let cbox = if self.is_column() {
-                PixelBox::from_origin_and_size(
-                    PixelPoint {
-                        x: inside.min.x,
-                        y: inside.min.y + last_offset,
-                        ..Default::default()
-                    },
-                    PixelSize {
-                        width: inside.width(),
-                        height: (offset - last_offset) as i32,
-                        ..Default::default()
-                    },
-                )
-            } else {
-                PixelBox::from_origin_and_size(
-                    PixelPoint {
-                        x: inside.min.x + last_offset,
-                        y: inside.min.y,
-                        ..Default::default()
-                    },
-                    PixelSize {
-                        width: (offset - last_offset) as i32,
-                        height: inside.height(),
-                        ..Default::default()
-                    },
-                )
-            };
-
-            self.layout_step(child, cbox);
-            last_offset = offset;
-        }
-    }
-}
+mod line;
+pub use line::LineArrangement;
+mod table;
+pub use table::TableArrangement;
 
 /// Calculate the fit of a number of size policies inside a given length.
 ///
@@ -139,7 +26,11 @@ fn do_fit<'l>(
 ) -> impl Iterator<Item = i32> {
     // first pass over children: collect sum total of minimum/desired sizes and/slack weights
     let policy_sum = policies.clone().reduce(SizePolicy::add).unwrap_or_default();
-    let (sum_min, sum_desired, sum_slack) = (policy_sum.minimum as i32, policy_sum.desired as i32, policy_sum.slack_weight as i32);
+    let (sum_min, sum_desired, sum_slack) = (
+        policy_sum.minimum as i32,
+        policy_sum.desired as i32,
+        policy_sum.slack_weight as i32,
+    );
 
     if total < sum_min {
         todo!("not enough space to distribute all children")
@@ -218,7 +109,11 @@ mod tests {
 
     #[test]
     fn distribute_3_even() {
-        let policies = vec![SizePolicy::expanding(1), SizePolicy::expanding(1), SizePolicy::expanding(1)];
+        let policies = vec![
+            SizePolicy::expanding(1),
+            SizePolicy::expanding(1),
+            SizePolicy::expanding(1),
+        ];
 
         assert_eq!(
             do_fit(3, policies.clone().into_iter()).collect::<Vec<_>>(),

+ 123 - 0
src/layout/arr/line.rs

@@ -0,0 +1,123 @@
+use super::{do_fit, ArrangementCalculator};
+use crate::{
+    layout::{cache::NodeState, LayoutNodeAccess, SizePolicy},
+    math::{PixelBox, PixelPoint, PixelSize},
+};
+use std::ops::Add;
+
+#[derive(Clone, Debug)]
+pub enum LineArrangement {
+    Column,
+    Row,
+}
+
+impl LineArrangement {
+    fn is_column(&self) -> bool {
+        match self {
+            Self::Column => true,
+            Self::Row => false,
+        }
+    }
+}
+
+impl ArrangementCalculator for LineArrangement {
+    fn arrange_step(
+        &self,
+        node: LayoutNodeAccess,
+        child_policies: Vec<(SizePolicy, SizePolicy)>,
+    ) -> (SizePolicy, SizePolicy) {
+        if child_policies.is_empty() {
+            return (node.width_policy, node.height_policy);
+        }
+
+        let cw_policy = child_policies
+            .iter()
+            .map(|v| v.0)
+            .reduce(if self.is_column() {
+                SizePolicy::max
+            } else {
+                SizePolicy::add
+            })
+            .unwrap();
+        let ch_policy: SizePolicy = child_policies
+            .iter()
+            .map(|v| v.1)
+            .reduce(if self.is_column() {
+                SizePolicy::add
+            } else {
+                SizePolicy::max
+            })
+            .unwrap();
+        (
+            node.width_policy.max_preserve_slack(cw_policy),
+            node.height_policy.max_preserve_slack(ch_policy),
+        )
+    }
+
+    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {
+        // do the final children layouts
+        node.cache.with_state(node.cache_key, |ns| {
+            if Some(inside) != ns.area {
+                ns.area = Some(inside);
+                ns.needs_render = true;
+            }
+        });
+
+        if node.child_len() == 0 {
+            return;
+        }
+
+        // expansion direction extraction lambda
+        let ee = if self.is_column() {
+            |ns: &mut NodeState| ns.net_policy.1
+        } else {
+            |ns: &mut NodeState| ns.net_policy.0
+        };
+
+        let policies = node
+            .child_iter()
+            .map(|c| node.cache.with_state(c.cache_key, ee).unwrap());
+        let mut last_offset = 0;
+        let fit = do_fit(
+            if self.is_column() {
+                inside.height() as i32
+            } else {
+                inside.width() as i32
+            },
+            policies,
+        );
+
+        for (offset, child) in fit.zip(node.child_iter()) {
+            let cbox = if self.is_column() {
+                PixelBox::from_origin_and_size(
+                    PixelPoint {
+                        x: inside.min.x,
+                        y: inside.min.y + last_offset,
+                        ..Default::default()
+                    },
+                    PixelSize {
+                        width: inside.width(),
+                        height: (offset - last_offset) as i32,
+                        ..Default::default()
+                    },
+                )
+            } else {
+                PixelBox::from_origin_and_size(
+                    PixelPoint {
+                        x: inside.min.x + last_offset,
+                        y: inside.min.y,
+                        ..Default::default()
+                    },
+                    PixelSize {
+                        width: (offset - last_offset) as i32,
+                        height: inside.height(),
+                        ..Default::default()
+                    },
+                )
+            };
+
+            self.layout_step(child, cbox);
+            last_offset = offset;
+        }
+    }
+}

+ 37 - 0
src/layout/arr/table.rs

@@ -0,0 +1,37 @@
+use super::{do_fit, ArrangementCalculator};
+use crate::{
+    layout::{cache::NodeState, LayoutNodeAccess, SizePolicy},
+    math::{PixelBox, PixelPoint, PixelSize},
+};
+use std::ops::Add;
+
+#[derive(Clone, Debug)]
+pub struct TableArrangement;
+
+impl TableArrangement {
+    fn max_coord(&self, node: &LayoutNodeAccess) -> Option<(usize, usize)> {
+        let Some(tc) = &node.table_cells else {
+            return None;
+        };
+        let mut max_row = None;
+        let mut max_col = None;
+        for cell in tc.iter() {
+            max_col = max_col.max(Some(cell.0 .0));
+            max_row = max_row.max(Some(cell.0 .1));
+        }
+        max_row.zip(max_col)
+    }
+}
+
+impl ArrangementCalculator for TableArrangement {
+    fn arrange_step(
+        &self,
+        node: LayoutNodeAccess,
+        child_policies: Vec<(SizePolicy, SizePolicy)>,
+    ) -> (SizePolicy, SizePolicy) {
+        // pull row/column information from child node properties
+        todo!()
+    }
+
+    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {}
+}

+ 20 - 22
src/layout/calc.rs

@@ -66,8 +66,8 @@ mod test {
     use kahlo::math::{PixelBox, PixelPoint, PixelRect, PixelSize};
 
     use crate::layout::{
-        cache::LayoutCache, ChildArrangement, LayoutNode, LayoutNodeAccess, LayoutNodeContainer,
-        NodeBehaviour, SizePolicy, dump_node_tree,
+        cache::LayoutCache, dump_node_tree, ChildArrangement, LayoutNode, LayoutNodeAccess,
+        LayoutNodeContainer, NodeBehaviour, SizePolicy,
     };
 
     use super::recalculate;
@@ -205,11 +205,19 @@ mod test {
             let area = PixelBox::from_origin_and_size(PixelPoint::new(1, 1), PixelSize::new(2, h));
             root.node.set_behaviour(NodeBehaviour::Fixed { area });
             recalculate(LayoutNodeAccess::new(&root));
-            
-            let child_areas = root.children.iter().map(|c| c.node.render_area().expect("layout did not give node a size")).collect::<Vec<_>>();
+
+            let child_areas = root
+                .children
+                .iter()
+                .map(|c| {
+                    c.node
+                        .render_area()
+                        .expect("layout did not give node a size")
+                })
+                .collect::<Vec<_>>();
 
             for i in 0..child_areas.len() {
-                for j in (i+1)..child_areas.len() {
+                for j in (i + 1)..child_areas.len() {
                     if child_areas[i].intersects(&child_areas[j]) {
                         panic!("layout children given overlapping areas!");
                     }
@@ -241,27 +249,17 @@ mod test {
         };
 
         let mut root = LayoutTree {
-            children: vec![
-                make_node(vec![
-                    make_node(vec![
-                        make_node(vec![
-                            make_node(vec![]),
-                            make_node(vec![]),
-                        ])
-                    ]),
-                    make_node(vec![
-                        make_node(vec![
-                            make_node(vec![]),
-                            make_node(vec![]),
-                        ])
-                    ])
-                ])
-            ],
+            children: vec![make_node(vec![
+                make_node(vec![make_node(vec![make_node(vec![]), make_node(vec![])])]),
+                make_node(vec![make_node(vec![make_node(vec![]), make_node(vec![])])]),
+            ])],
             node: LayoutNode::new(cache.clone()),
         };
 
         root.node.child_arrangement = ChildArrangement::Column;
-        root.node.set_behaviour(NodeBehaviour::Fixed { area: PixelBox::from_size(PixelSize::new(40, 40)) });
+        root.node.set_behaviour(NodeBehaviour::Fixed {
+            area: PixelBox::from_size(PixelSize::new(40, 40)),
+        });
         let mut dump = String::new();
         dump_node_tree(LayoutNodeAccess::new(&root), &mut dump);
         println!("before recalculate:\n{dump}");

+ 0 - 1
src/theme.rs

@@ -29,7 +29,6 @@ impl Theme {
             foreground: Colour::WHITE,
 
             // other colour in palette: #F2F3D9
-
             ui_font,
             ui_font_size: 16.0,
         }

+ 6 - 3
src/ui.rs

@@ -169,7 +169,10 @@ impl<UIC: UIComponent> winit::application::ApplicationHandler<()> for UI<UIC> {
                 self.state.window_states.remove(&window_id);
             }
             winit::event::WindowEvent::RedrawRequested => {
-                wsa.redraw(&UIHandle { eloop: event_loop, state: &mut self.state });
+                wsa.redraw(&UIHandle {
+                    eloop: event_loop,
+                    state: &mut self.state,
+                });
                 self.pump_events(event_loop);
             }
             winit::event::WindowEvent::CursorMoved { position, .. } => {
@@ -190,8 +193,8 @@ impl<UIC: UIComponent> winit::application::ApplicationHandler<()> for UI<UIC> {
             winit::event::WindowEvent::Resized(newsize) => {
                 wsa.notify_resize(PixelSize::new(newsize.width as i32, newsize.height as i32));
                 wsa.request_redraw();
-            },
-            _ => { },
+            }
+            _ => {}
         }
     }
 }

+ 12 - 4
src/widget/button.rs

@@ -1,10 +1,14 @@
-use kahlo::{prelude::*, math::PixelSideOffsets};
+use kahlo::{math::PixelSideOffsets, prelude::*};
 
 use crate::{
     component::Component,
     input::MouseButton,
-    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy, HorizontalAlignment, VerticalAlignment},
-    ui::UIHandle, window::RenderFormat,
+    layout::{
+        HorizontalAlignment, LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy,
+        VerticalAlignment,
+    },
+    ui::UIHandle,
+    window::RenderFormat,
 };
 
 use super::{Label, Widget};
@@ -112,7 +116,11 @@ impl<C: Component> Widget<C> for Button<C> {
         };
         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);
+            target.rectangle(
+                self.layout.render_area().unwrap(),
+                theme.border_width,
+                theme.border,
+            );
         }
         self.label.render(theme, target);
     }

+ 2 - 1
src/widget/frame.rs

@@ -4,7 +4,8 @@ use crate::{
     component::Component,
     layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy},
     theme::Theme,
-    ui::UIHandle, window::RenderFormat,
+    ui::UIHandle,
+    window::RenderFormat,
 };
 
 use super::Widget;

+ 77 - 3
src/widget/group.rs

@@ -1,10 +1,11 @@
 use crate::{
     component::Component,
     input::InputState,
-    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer},
+    layout::{ChildArrangement, LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy},
     theme::Theme,
     ui::UIHandle,
-    widget::Widget, window::RenderFormat,
+    widget::Widget,
+    window::RenderFormat,
 };
 
 pub struct PlainGroup<C: Component> {
@@ -15,14 +16,41 @@ pub struct PlainGroup<C: Component> {
 impl<C: Component> PlainGroup<C> {
     pub fn new(uih: &UIHandle) -> Self {
         Self {
-            lnode: uih.new_layout_node(),
+            lnode: {
+                let mut node = uih.new_layout_node();
+                node.set_width_policy(SizePolicy::expanding(1));
+                node.set_height_policy(SizePolicy::expanding(1));
+                node
+            },
             children: vec![],
         }
     }
 
+    pub fn new_column(uih: &UIHandle) -> Self {
+        let mut ret = Self::new(uih);
+        ret.lnode.set_arrangement(ChildArrangement::Column);
+        ret
+    }
+
+    pub fn new_row(uih: &UIHandle) -> Self {
+        let mut ret = Self::new(uih);
+        ret.lnode.set_arrangement(ChildArrangement::Row);
+        ret
+    }
+
+    pub fn new_table(uih: &UIHandle) -> Self {
+        let mut ret = Self::new(uih);
+        ret.lnode.set_arrangement(ChildArrangement::Table);
+        ret
+    }
+
     pub fn append(&mut self, widget: Box<dyn Widget<C>>) {
         self.children.push(widget);
     }
+
+    pub fn extend(&mut self, with: impl Iterator<Item = Box<dyn Widget<C>>>) {
+        self.children.extend(with);
+    }
 }
 
 impl<C: Component> LayoutNodeContainer for PlainGroup<C> {
@@ -59,3 +87,49 @@ impl<C: Component> Widget<C> for PlainGroup<C> {
         }
     }
 }
+
+use super::Label;
+
+pub struct FormGroup<C: Component> {
+    lnode: LayoutNode,
+    labelnode: LayoutNode,
+    widgetnode: LayoutNode,
+    labels: Vec<Label<C>>,
+    widgets: Vec<Box<dyn Widget<C>>>,
+}
+
+struct LabelContainer<'l, C: Component>(&'l FormGroup<C>);
+struct WidgetContainer<'l, C: Component>(&'l FormGroup<C>);
+
+impl<C: Component> LayoutNodeContainer for FormGroup<C> {
+    fn layout_node(&self) -> &LayoutNode {
+        &self.lnode
+    }
+    fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess<'_>> {
+        match ndx {
+            0 => todo!(), // Some(LayoutNodeAccess::new(LabelContainer(self))),
+            1 => todo!(),
+            _ => None,
+        }
+    }
+    fn layout_child_count(&self) -> usize {
+        2
+    }
+}
+
+impl<C: Component> Widget<C> for FormGroup<C> {
+    fn layout_node(&self) -> LayoutNodeAccess {
+        todo!()
+    }
+    fn layout_node_mut(&mut self) -> &mut LayoutNode {
+        todo!()
+    }
+    fn poll(
+        &mut self,
+        uih: &mut UIHandle,
+        input_state: Option<&InputState>,
+    ) -> Vec<<C as Component>::Msg> {
+        todo!()
+    }
+    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {}
+}

+ 20 - 7
src/widget/label.rs

@@ -1,12 +1,16 @@
 use std::cell::RefCell;
 
-use kahlo::{math::{PixelBox, PixelSideOffsets}, prelude::*};
+use kahlo::{
+    math::{PixelBox, PixelSideOffsets},
+    prelude::*,
+};
 
 use crate::{
     component::Component,
-    layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy, HorizontalAlignment},
+    layout::{HorizontalAlignment, LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
     theme::Theme,
-    ui::UIHandle, window::RenderFormat,
+    ui::UIHandle,
+    window::RenderFormat,
 };
 
 use super::Widget;
@@ -75,7 +79,7 @@ impl<C: Component> Widget<C> for Label<C> {
 
     fn render(&self, theme: &Theme, surface: &mut kahlo::BitmapMut<RenderFormat>) {
         if !self.lnode.render_check() {
-            return
+            return;
         }
         let rcon = self.rendered.borrow();
         let Some(rendered) = rcon.as_ref() else {
@@ -86,9 +90,18 @@ impl<C: Component> Widget<C> for Label<C> {
         // perform horizontal alignment
         let alignment_slack = (render_location.width() - rendered.size().width).max(0);
         let render_location = match self.lnode.halign() {
-            HorizontalAlignment::Left => render_location.inner_box(PixelSideOffsets::new(0, alignment_slack, 0, 0)),
-            HorizontalAlignment::Centre => render_location.inner_box(PixelSideOffsets::new(0, alignment_slack / 2, 0, alignment_slack / 2)),
-            HorizontalAlignment::Right => render_location.inner_box(PixelSideOffsets::new(0, 0, 0, alignment_slack)),
+            HorizontalAlignment::Left => {
+                render_location.inner_box(PixelSideOffsets::new(0, alignment_slack, 0, 0))
+            }
+            HorizontalAlignment::Centre => render_location.inner_box(PixelSideOffsets::new(
+                0,
+                alignment_slack / 2,
+                0,
+                alignment_slack / 2,
+            )),
+            HorizontalAlignment::Right => {
+                render_location.inner_box(PixelSideOffsets::new(0, 0, 0, alignment_slack))
+            }
         };
 
         surface.fill_region_masked(

+ 68 - 9
src/window.rs

@@ -28,6 +28,7 @@ pub trait WindowComponent: 'static + Sized + Component {
 }
 
 pub(crate) struct WindowState<WC: WindowComponent> {
+    render_generation: usize,
     wc: WC,
     root_node: LayoutNode,
     window: Rc<winit::window::Window>,
@@ -56,13 +57,14 @@ impl<WC: WindowComponent> WindowState<WC> {
         let layout = LinearAccess::new(&self.root_node, self.wc.root_widget().layout_node());
 
         let mut bitmap = self.bitmap.borrow_mut();
-        if bitmap.as_ref().map(|v| v.size()) != Some(PixelSize::new(size.width as i32, size.height as i32)) {
+        if bitmap.as_ref().map(|v| v.size())
+            != Some(PixelSize::new(size.width as i32, size.height as i32))
+        {
             bitmap.take();
         }
 
-        let bitmap = bitmap.get_or_insert_with(|| {
-            kahlo::Bitmap::new(size.width as usize, size.height as usize)
-        });
+        let bitmap = bitmap
+            .get_or_insert_with(|| kahlo::Bitmap::new(size.width as usize, size.height as usize));
 
         let mut windowbuffer = kahlo::BitmapMut::<kahlo::formats::Bgr32>::new(
             unsafe {
@@ -74,14 +76,62 @@ impl<WC: WindowComponent> WindowState<WC> {
 
         layout::recalculate(LayoutNodeAccess::new(&layout));
         let before = std::time::Instant::now();
-        self.wc.root_widget().render(uih.theme(), &mut bitmap.as_mut());
+
+        // save the layout tree for later dumping
+        let mut pre_render_dump = format!("render generation: {}\n", self.render_generation);
+        {
+            self.render_generation += 1;
+            crate::layout::dump_node_tree(LayoutNodeAccess::new(&layout), &mut pre_render_dump);
+        }
+
+        self.wc
+            .root_widget()
+            .render(uih.theme(), &mut bitmap.as_mut());
         windowbuffer.copy_from(
             bitmap,
             PixelBox::from_size(bitmap.size()),
-            PixelPoint::zero()
+            PixelPoint::zero(),
         );
         let after = std::time::Instant::now();
-        print!("render time: {:?}        \r", (after - before));
+
+        // put the render time on the screen
+        // we're going to do this the very dumb way for now
+        let render_time = format!(
+            "time: {:.03}ms",
+            (after - before).as_micros() as f32 / 1000.0
+        );
+        let line = uih.theme().make_line(render_time.as_str()).render_line();
+
+        windowbuffer.fill_region_masked(
+            &line,
+            PixelBox::from_size(line.size()),
+            PixelPoint::new(
+                size.width as i32 - line.size().width,
+                size.height as i32 - line.size().height,
+            ),
+            uih.theme().foreground,
+            kahlo::colour::BlendMode::Simple,
+        );
+
+        // now put the pre-render layout dump on the window
+        // again, we're doing this the dumb way
+        let mut offset = 0;
+        for text in pre_render_dump.split("\n") {
+            if text.len() == 0 {
+                continue;
+            }
+            let line = uih.theme().make_line(text).render_line();
+
+            windowbuffer.fill_region_masked(
+                &line,
+                PixelBox::from_size(line.size()),
+                PixelPoint::new(0, offset),
+                uih.theme().foreground,
+                kahlo::colour::BlendMode::Simple,
+            );
+
+            offset += line.height() as i32 + 1;
+        }
 
         buf.present().unwrap();
     }
@@ -115,8 +165,16 @@ impl<WC: WindowComponent> WindowStateAccess for RefCell<WindowState<WC>> {
     fn notify_resize(&self, new_size: PixelSize) {
         if Some(new_size) != self.borrow().bitmap.borrow().as_ref().map(|v| v.size()) {
             self.borrow_mut().bitmap.take();
-            self.borrow_mut().wc.root_widget().layout_node().relayout_tree();
-            self.borrow_mut().wc.root_widget().layout_node().render_needed();
+            self.borrow_mut()
+                .wc
+                .root_widget()
+                .layout_node()
+                .relayout_tree();
+            self.borrow_mut()
+                .wc
+                .root_widget()
+                .layout_node()
+                .render_needed();
         }
     }
     fn push_event(&self, we: WindowEvent) {
@@ -160,6 +218,7 @@ impl<'r, 'l: 'r> WindowBuilder<'r, 'l> {
 
         let wc = wc(self.ui_handle);
         let wstate = Rc::new(RefCell::new(WindowState {
+            render_generation: 0,
             wc,
             root_node: LayoutNode::new(self.ui_handle.state.layout_cache.clone()),
             window,