2 次代碼提交 7f638e7456 ... 1db569b8bd

作者 SHA1 備註 提交日期
  Kestrel 1db569b8bd Working node alignment and node margins. 3 月之前
  Kestrel 5fe6e9d99a First steps towards supporting node alignment. 3 月之前
共有 11 個文件被更改,包括 243 次插入104 次删除
  1. 21 9
      examples/table_layout.rs
  2. 45 16
      src/layout.rs
  3. 42 3
      src/layout/arr.rs
  4. 39 0
      src/layout/arr/container.rs
  5. 19 22
      src/layout/arr/line.rs
  6. 19 12
      src/layout/arr/table.rs
  7. 2 2
      src/layout/cache.rs
  8. 9 15
      src/layout/calc.rs
  9. 35 1
      src/widget.rs
  10. 3 1
      src/widget/frame.rs
  11. 9 23
      src/widget/label.rs

+ 21 - 9
examples/table_layout.rs

@@ -1,5 +1,6 @@
+use kahlo::math::PixelSideOffsets;
 use patina::{
-    layout::TableCell,
+    layout::{HorizontalAlignment, TableCell, VerticalAlignment},
     prelude::*,
     ui::{UIControlMsg, UIHandle},
     widget,
@@ -14,24 +15,31 @@ impl TableWindow {
         let mut group = widget::PlainGroup::new_table(uih);
         group.extend(
             vec![
-                widget::Label::new_with_text(uih, "r1c1")
-                    .framed()
+                widget::Label::new_with_text(uih, "x0y0")
+                    .with_valign(VerticalAlignment::Centre)
+                    // .framed()
                     .with_table_cell(TableCell::new(0, 0))
                     .boxed(),
-                widget::Label::new_with_text(uih, "r2c1")
+                widget::Label::new_with_text(uih, "x0y1 [left aligned]")
+                    .with_halign(HorizontalAlignment::Left)
+                    .with_valign(VerticalAlignment::Centre)
                     .framed()
+                    .with_margins(PixelSideOffsets::new_all_same(15))
                     .with_table_cell(TableCell::new(0, 1))
                     .boxed(),
-                widget::Label::new_with_text(uih, "r2c2")
-                    .framed()
+                widget::Label::new_with_text(uih, "x1y1")
+                    .with_valign(VerticalAlignment::Centre)
+                    // .framed()
                     .with_table_cell(TableCell::new(1, 1))
                     .boxed(),
-                widget::Label::new_with_text(uih, "r2c2")
+                widget::Label::new_with_text(uih, "x1y2")
+                    .with_valign(VerticalAlignment::Centre)
                     .framed()
                     .with_table_cell(TableCell::new(1, 2))
                     .boxed(),
-                widget::Label::new_with_text(uih, "r3c2")
-                    .framed()
+                widget::Label::new_with_text(uih, "x3y2")
+                    .with_valign(VerticalAlignment::Centre)
+                    // .framed()
                     .with_table_cell(TableCell::new(3, 2))
                     .boxed(),
             ]
@@ -46,6 +54,10 @@ impl Component for TableWindow {
     type Msg = ();
 
     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-table-layout-example.layout_tree", out)
+            .expect("couldn't dump node tree");
         vec![UIControlMsg::Terminate]
     }
 }

+ 45 - 16
src/layout.rs

@@ -71,6 +71,20 @@ impl SizePolicy {
         }
     }
 
+    pub fn with_minimum(mut self, new_min: usize) -> Self {
+        self.minimum = new_min;
+        self.desired = self.desired.max(new_min);
+        self
+    }
+    pub fn with_desired(mut self, new_desire: usize) -> Self {
+        self.desired = new_desire;
+        self
+    }
+    pub fn with_slack(mut self, new_slack: usize) -> Self {
+        self.slack_weight = new_slack;
+        self
+    }
+
     pub fn max(self, rhs: SizePolicy) -> SizePolicy {
         Self {
             minimum: self.minimum.max(rhs.minimum),
@@ -88,6 +102,9 @@ impl SizePolicy {
     }
 }
 
+pub struct SizePolicyTag;
+pub type SizePolicy2D = euclid::Size2D<SizePolicy, SizePolicyTag>;
+
 impl std::ops::Add<Self> for SizePolicy {
     type Output = Self;
     fn add(self, rhs: Self) -> Self::Output {
@@ -141,6 +158,7 @@ pub enum NodeBehaviour {
 #[derive(Clone)]
 pub enum ChildArrangement {
     Custom(std::rc::Rc<dyn arr::ArrangementCalculator>),
+    Container,
     Column,
     Row,
     Table,
@@ -150,6 +168,7 @@ impl std::fmt::Debug for ChildArrangement {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             Self::Custom(_) => f.write_str("ChildArrangement::Custom"),
+            Self::Container => f.write_str("ChildArrangement::Container"),
             Self::Column => f.write_str("ChildArrangement::Column"),
             Self::Row => f.write_str("ChildArrangement::Row"),
             Self::Table => f.write_str("ChildArrangement::Table"),
@@ -162,6 +181,7 @@ impl Deref for ChildArrangement {
     fn deref(&self) -> &Self::Target {
         match self {
             Self::Custom(calc) => calc.as_ref(),
+            Self::Container => &arr::ContainerArrangement,
             Self::Column => &arr::LineArrangement::Column,
             Self::Row => &arr::LineArrangement::Row,
             Self::Table => &arr::TableArrangement,
@@ -184,13 +204,13 @@ pub struct LayoutNode {
     width_policy: SizePolicy,
     /// Height policy: how does this widget take up vertical space?
     height_policy: SizePolicy,
-    /// Horizontal alignment of children inside this node.
+    /// Horizontal alignment of this node inside its parent.
     halign: HorizontalAlignment,
-    /// Vertical alignment of children inside this node.
+    /// Vertical alignment of this node inside its parent.
     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 row/column assignment for this node insite its parent.
     table_cell: Option<TableCell>,
 }
 
@@ -231,6 +251,10 @@ impl LayoutNode {
         &self.cache
     }
 
+    fn with_state<R, F: FnOnce(&mut NodeState) -> R>(&self, f: F) -> Option<R> {
+        self.cache.with_state(self.cache_key, f)
+    }
+
     pub fn relayout(&self) {
         self.cache.update_queue.borrow_mut().push(self.cache_key);
     }
@@ -246,21 +270,17 @@ impl LayoutNode {
     }
 
     pub fn render_area(&self) -> Option<PixelBox> {
-        self.cache
-            .with_state(self.cache_key, |ns| ns.area)
-            .flatten()
-            .map(|pb| pb.inner_box(self.margin))
+        self.with_state(|ns| ns.area).flatten()
     }
 
     /// 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.with_state(|ns| {
+            let ret = ns.needs_render;
+            ns.needs_render = false;
+            ret
+        })
+        .unwrap_or(false)
     }
 
     pub fn render_needed(&self) {
@@ -335,6 +355,12 @@ impl LayoutNode {
     pub fn margin_mut(&mut self) -> &mut PixelSideOffsets {
         &mut self.margin
     }
+    pub fn margin_as_policy(&self) -> SizePolicy2D {
+        SizePolicy2D::new(
+            SizePolicy::fixed((self.margin.left + self.margin.right) as usize),
+            SizePolicy::fixed((self.margin.top + self.margin.bottom) as usize),
+        )
+    }
 
     pub fn table_cell(&self) -> &Option<TableCell> {
         &self.table_cell
@@ -386,13 +412,16 @@ fn dump_node_tree_helper(lna: LayoutNodeAccess, indent: usize, out: &mut String)
             lna.height_policy.desired,
             lna.height_policy.slack_weight,
             lna.behaviour,
-            lna.cache.with_state(lna.cache_key, |v| v.needs_update),
-            lna.cache.with_state(lna.cache_key, |v| v.needs_render),
+            lna.with_state(|v| v.needs_update),
+            lna.with_state(|v| v.needs_render),
         )
         .as_str(),
     );
     out.push_str(ind.as_str());
     out.push_str("        ");
+    out.push_str(format!("net policy:  {:?}\n", lna.with_state(|v| v.net_policy)).as_str());
+    out.push_str(ind.as_str());
+    out.push_str("        ");
     out.push_str(format!("render area: {:?}\n", lna.render_area()).as_str());
 
     for child in lna.child_iter() {

+ 42 - 3
src/layout/arr.rs

@@ -1,7 +1,7 @@
-use kahlo::math::PixelBox;
+use kahlo::math::{PixelBox, PixelSideOffsets};
 use std::ops::Add;
 
-use super::{LayoutNodeAccess, SizePolicy};
+use super::{HorizontalAlignment, LayoutNodeAccess, SizePolicy, VerticalAlignment, SizePolicy2D};
 
 /// Child arrangement calculator trait.
 ///
@@ -10,15 +10,54 @@ use super::{LayoutNodeAccess, SizePolicy};
 /// node. The layout step then uses the computed constraints and size policies to compute a set of
 /// pixel-space rects.
 pub trait ArrangementCalculator {
-    fn arrange_step(&self, node: LayoutNodeAccess) -> (SizePolicy, SizePolicy);
+    fn arrange_step(&self, node: LayoutNodeAccess) -> SizePolicy2D;
     fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox);
 }
 
+mod container;
+pub use container::ContainerArrangement;
 mod line;
 pub use line::LineArrangement;
 mod table;
 pub use table::TableArrangement;
 
+fn do_align(node: &LayoutNodeAccess, mut inside: PixelBox) -> PixelBox {
+    let net_policy = node
+        .with_state(|ns| ns.net_policy)
+        .expect("do_align invoked with node that has no net_policy");
+    println!("do_align | net: {net_policy:?}");
+    println!(
+        "         | alignments: {:?} {:?}",
+        node.halign(),
+        node.valign()
+    );
+    println!("         | inside before: {inside:?}");
+    if net_policy.width.slack_weight == 0 {
+        let slack = (inside.width() - net_policy.width.desired as i32).max(0);
+        let hshrink = match node.halign() {
+            HorizontalAlignment::Left => PixelSideOffsets::new(0, slack, 0, 0),
+            HorizontalAlignment::Centre => {
+                PixelSideOffsets::new(0, slack / 2, 0, slack - (slack / 2))
+            }
+            HorizontalAlignment::Right => PixelSideOffsets::new(0, 0, 0, slack),
+        };
+        inside = inside.inner_box(hshrink);
+    }
+    if net_policy.height.slack_weight == 0 {
+        let slack = (inside.height() - net_policy.height.desired as i32).max(0);
+        let vshrink = match node.valign() {
+            VerticalAlignment::Top => PixelSideOffsets::new(0, 0, slack, 0),
+            VerticalAlignment::Centre => {
+                PixelSideOffsets::new(slack / 2, 0, slack - (slack / 2), 0)
+            }
+            VerticalAlignment::Bottom => PixelSideOffsets::new(slack, 0, 0, 0),
+        };
+        inside = inside.inner_box(vshrink);
+    }
+    println!("         | inside after:  {inside:?}");
+    inside
+}
+
 /// Calculate the fit of a number of size policies inside a given length.
 ///
 /// Returns the endpoints of each item.

+ 39 - 0
src/layout/arr/container.rs

@@ -0,0 +1,39 @@
+use super::ArrangementCalculator;
+use crate::{
+    layout::{LayoutNodeAccess, SizePolicy2D},
+    math::PixelBox,
+};
+
+#[derive(Clone, Debug)]
+pub struct ContainerArrangement;
+
+impl ArrangementCalculator for ContainerArrangement {
+    fn arrange_step(&self, node: LayoutNodeAccess) -> SizePolicy2D {
+        let Some(child) = node.child(0) else {
+            return (node.width_policy(), node.height_policy()).into();
+        };
+        SizePolicy2D::new(
+            node.width_policy.max_preserve_slack(child.width_policy()),
+            node.height_policy.max_preserve_slack(child.height_policy()),
+        ) + node.margin_as_policy()
+    }
+
+    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {
+        let inside = super::do_align(&node, inside);
+
+        node.with_state(|ns| {
+            if Some(inside) != ns.area {
+                ns.area = Some(inside);
+                ns.needs_render = true;
+            }
+        });
+
+        let Some(child) = node.child(0) else {
+            return;
+        };
+
+        child
+            .child_arrangement
+            .layout_step(child, inside.inner_box(node.margin()));
+    }
+}

+ 19 - 22
src/layout/arr/line.rs

@@ -1,9 +1,6 @@
 use super::{do_fit, ArrangementCalculator};
 use crate::{
-    layout::{
-        cache::{LayoutCacheKey, NodeState},
-        LayoutNodeAccess, SizePolicy,
-    },
+    layout::{cache::NodeState, LayoutNodeAccess, SizePolicy, SizePolicy2D},
     math::{PixelBox, PixelPoint, PixelSize},
 };
 use std::ops::Add;
@@ -24,23 +21,19 @@ impl LineArrangement {
 }
 
 impl ArrangementCalculator for LineArrangement {
-    fn arrange_step(&self, node: LayoutNodeAccess) -> (SizePolicy, SizePolicy) {
+    fn arrange_step(&self, node: LayoutNodeAccess) -> SizePolicy2D {
         if node.child_len() == 0 {
-            return (node.width_policy, node.height_policy);
+            return (node.width_policy, node.height_policy).into();
         }
 
         let child_policies = node
             .child_iter()
-            .map(|child| {
-                node.cache
-                    .with_state(child.cache_key, |s| s.net_policy)
-                    .unwrap()
-            })
+            .map(|child| child.with_state(|s| s.net_policy).unwrap())
             .collect::<Vec<_>>();
 
         let cw_policy = child_policies
             .iter()
-            .map(|v| v.0)
+            .map(|v| v.width)
             .reduce(if self.is_column() {
                 SizePolicy::max
             } else {
@@ -49,22 +42,23 @@ impl ArrangementCalculator for LineArrangement {
             .unwrap();
         let ch_policy: SizePolicy = child_policies
             .iter()
-            .map(|v| v.1)
+            .map(|v| v.height)
             .reduce(if self.is_column() {
                 SizePolicy::add
             } else {
                 SizePolicy::max
             })
             .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()
     }
 
     fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {
+        let inside = super::do_align(&node, inside);
         // do the final children layouts
-        node.cache.with_state(node.cache_key, |ns| {
+        node.with_state(|ns| {
             if Some(inside) != ns.area {
                 ns.area = Some(inside);
                 ns.needs_render = true;
@@ -75,16 +69,17 @@ impl ArrangementCalculator for LineArrangement {
             return;
         }
 
+        // add margins
+        let inside = inside.inner_box(node.margin);
+
         // expansion direction extraction lambda
         let ee = if self.is_column() {
-            |ns: &mut NodeState| ns.net_policy.1
+            |ns: &mut NodeState| ns.net_policy.height
         } else {
-            |ns: &mut NodeState| ns.net_policy.0
+            |ns: &mut NodeState| ns.net_policy.width
         };
 
-        let policies = node
-            .child_iter()
-            .map(|c| node.cache.with_state(c.cache_key, ee).unwrap());
+        let policies = node.child_iter().map(|c| c.with_state(ee).unwrap());
         let mut last_offset = 0;
         let fit = do_fit(
             if self.is_column() {
@@ -95,6 +90,8 @@ impl ArrangementCalculator for LineArrangement {
             policies,
         );
 
+        // XXX: do 'alignment' of child stack here via an offset?
+
         for (offset, child) in fit.zip(node.child_iter()) {
             let cbox = if self.is_column() {
                 PixelBox::from_origin_and_size(

+ 19 - 12
src/layout/arr/table.rs

@@ -2,7 +2,7 @@ use super::{do_fit, ArrangementCalculator};
 use crate::{
     layout::{
         cache::{LayoutCacheKey, NodeState},
-        LayoutNodeAccess, SizePolicy, TableCell, TableSize,
+        LayoutNodeAccess, SizePolicy, TableCell, TableSize, SizePolicy2D,
     },
     math::{PixelBox, PixelPoint, PixelSize},
 };
@@ -48,12 +48,9 @@ impl TableArrangement {
                 tstate.row_policies.resize(cell.y + 1, SizePolicy::zero());
             }
 
-            let child_policy = node
-                .cache
-                .with_state(ch.cache_key, |v| v.net_policy)
-                .unwrap();
-            tstate.col_policies[cell.x] = tstate.col_policies[cell.x].max(child_policy.0);
-            tstate.row_policies[cell.y] = tstate.row_policies[cell.y].max(child_policy.1);
+            let child_policy = ch.with_state(|v| v.net_policy).unwrap();
+            tstate.col_policies[cell.x] = tstate.col_policies[cell.x].max(child_policy.width);
+            tstate.row_policies[cell.y] = tstate.row_policies[cell.y].max(child_policy.height);
         }
 
         tstate
@@ -61,13 +58,12 @@ impl TableArrangement {
 }
 
 impl ArrangementCalculator for TableArrangement {
-    fn arrange_step(&self, node: LayoutNodeAccess) -> (SizePolicy, SizePolicy) {
+    fn arrange_step(&self, node: LayoutNodeAccess) -> SizePolicy2D {
         let tstate = self.build_table_state(&node);
 
         // if there are no table cells...
         if tstate.row_policies.len() == 0 || tstate.col_policies.len() == 0 {
-            println!("returning early");
-            return (node.width_policy(), node.height_policy());
+            return (node.width_policy(), node.height_policy()).into();
         }
 
         let net_rows = tstate
@@ -81,13 +77,24 @@ impl ArrangementCalculator for TableArrangement {
             .reduce(SizePolicy::add)
             .unwrap();
 
-        (
+        SizePolicy2D::new(
             node.width_policy.max_preserve_slack(net_cols),
             node.height_policy.max_preserve_slack(net_rows),
-        )
+        ) + node.margin_as_policy()
     }
 
     fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {
+        let inside = super::do_align(&node, inside);
+
+        node.with_state(|ns| {
+            if Some(inside) != ns.area {
+                ns.area = Some(inside);
+                ns.needs_render = true;
+            }
+        });
+
+        let inside = inside.inner_box(node.margin());
+
         let tstate = self.build_table_state(&node);
 
         let mut col_offsets = vec![0];

+ 2 - 2
src/layout/cache.rs

@@ -1,6 +1,6 @@
 use std::{cell::RefCell, collections::HashMap, rc::Rc};
 
-use super::{SizePolicy, TableCell};
+use super::{SizePolicy, TableCell, SizePolicy2D};
 
 #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
 pub struct LayoutCacheKey(usize);
@@ -15,7 +15,7 @@ impl LayoutCacheKey {
 pub struct NodeState {
     pub(super) needs_update: bool,
     pub(super) needs_render: bool,
-    pub(super) net_policy: (SizePolicy, SizePolicy),
+    pub(super) net_policy: SizePolicy2D,
     pub(super) layer: Layer,
     pub(super) area: Option<kahlo::math::PixelBox>,
     pub(super) children: Vec<LayoutCacheKey>,

+ 9 - 15
src/layout/calc.rs

@@ -19,7 +19,7 @@ pub fn recalculate(node: LayoutNodeAccess) {
 
 fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, layer: Layer) {
     // early-out check
-    if node.cache.with_state(node.cache_key, |st| st.needs_update) == Some(false) {
+    if node.with_state(|st| st.needs_update) == Some(false) {
         return;
     }
 
@@ -28,12 +28,12 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, laye
     }
 
     // construct net size policy
-    let (wpol, hpol) = node.child_arrangement.arrange_step(node);
+    let net_policy = node.child_arrangement.arrange_step(node);
 
     let child_ids = node.child_iter().map(|ch| ch.cache_key).collect();
     if node.cache.has_state_for(node.cache_key) {
-        node.cache.with_state(node.cache_key, |ns| {
-            ns.net_policy = (wpol, hpol);
+        node.with_state(move |ns| {
+            ns.net_policy = net_policy;
             ns.children = child_ids;
         });
     } else {
@@ -43,7 +43,7 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, laye
             NodeState {
                 needs_update: false,
                 needs_render: true,
-                net_policy: (wpol, hpol),
+                net_policy,
                 area: None,
                 layer,
                 children: child_ids,
@@ -55,7 +55,7 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, laye
 
 #[cfg(test)]
 mod test {
-    use kahlo::math::{PixelBox, PixelPoint, PixelRect, PixelSize};
+    use kahlo::math::{PixelBox, PixelPoint, PixelSize};
 
     use crate::layout::{
         cache::LayoutCache, dump_node_tree, ChildArrangement, LayoutNode, LayoutNodeAccess,
@@ -125,9 +125,7 @@ mod test {
 
         // check that final rects match expectations
         assert_eq!(
-            cache
-                .with_state(root.node.cache_key, |ns| ns.area)
-                .flatten(),
+            root.node.with_state(|ns| ns.area).flatten(),
             Some(PixelBox::from_origin_and_size(
                 PixelPoint::new(1, 1),
                 PixelSize::new(2, 5)
@@ -135,9 +133,7 @@ mod test {
         );
 
         assert_eq!(
-            cache
-                .with_state(root.children[0].node.cache_key, |ns| ns.area)
-                .flatten(),
+            root.children[0].node.with_state(|ns| ns.area).flatten(),
             Some(PixelBox::from_origin_and_size(
                 PixelPoint::new(1, 1),
                 PixelSize::new(2, 2)
@@ -349,9 +345,7 @@ mod test {
         println!("after recalculate:\n{dump}");
 
         assert_eq!(
-            cache
-                .with_state(root.children[0].node.cache_key, |ns| ns.area)
-                .flatten(),
+            root.children[0].node.with_state(|ns| ns.area).flatten(),
             Some(PixelBox::from_origin_and_size(
                 PixelPoint::new(0, 0),
                 PixelSize::new(8, 8)

+ 35 - 1
src/widget.rs

@@ -1,13 +1,14 @@
 use crate::{
     component::Component,
     input::InputState,
-    layout::{LayoutNode, LayoutNodeAccess, TableCell},
+    layout::{HorizontalAlignment, LayoutNode, LayoutNodeAccess, TableCell, VerticalAlignment},
     theme::Theme,
     ui::UIHandle,
     window::RenderFormat,
 };
 
 mod label;
+use kahlo::math::PixelSideOffsets;
 pub use label::Label;
 mod frame;
 pub use frame::Frame;
@@ -41,6 +42,39 @@ pub trait WidgetExt<C: Component>: Widget<C> {
         Frame::wrap_widget(self)
     }
 
+    fn set_margins(&mut self, margins: PixelSideOffsets) {
+        *self.layout_node_mut().margin_mut() = margins;
+    }
+    fn with_margins(mut self, margins: PixelSideOffsets) -> Self
+    where
+        Self: Sized,
+    {
+        self.set_margins(margins);
+        self
+    }
+
+    fn set_halign(&mut self, halign: HorizontalAlignment) {
+        self.layout_node_mut().set_halign(halign);
+    }
+    fn with_halign(mut self, halign: HorizontalAlignment) -> Self
+    where
+        Self: Sized,
+    {
+        self.set_halign(halign);
+        self
+    }
+
+    fn set_valign(&mut self, valign: VerticalAlignment) {
+        self.layout_node_mut().set_valign(valign);
+    }
+    fn with_valign(mut self, valign: VerticalAlignment) -> Self
+    where
+        Self: Sized,
+    {
+        self.set_valign(valign);
+        self
+    }
+
     fn clear_table_cell(&mut self) {
         self.layout_node_mut().set_table_cell(None);
     }

+ 3 - 1
src/widget/frame.rs

@@ -2,7 +2,7 @@ use kahlo::{math::PixelSideOffsets, prelude::*};
 
 use crate::{
     component::Component,
-    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy},
+    layout::{ChildArrangement, LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy},
     theme::Theme,
     ui::UIHandle,
     window::RenderFormat,
@@ -19,6 +19,7 @@ impl<C: Component> Frame<C> {
     pub fn new(uih: &UIHandle) -> Self {
         let mut nnode = uih.new_layout_node();
         nnode
+            .set_arrangement(ChildArrangement::Container)
             .set_height_policy(SizePolicy::expanding(1))
             .set_width_policy(SizePolicy::expanding(1));
         Self {
@@ -36,6 +37,7 @@ impl<C: Component> Frame<C> {
     pub fn wrap_widget<W: 'static + Widget<C>>(child: W) -> Self {
         let mut nnode = LayoutNode::new(child.layout_node().cache().clone());
         nnode
+            .set_arrangement(ChildArrangement::Container)
             .set_height_policy(SizePolicy::expanding(1))
             .set_width_policy(SizePolicy::expanding(1));
         Self {

+ 9 - 23
src/widget/label.rs

@@ -1,13 +1,10 @@
 use std::cell::RefCell;
 
-use kahlo::{
-    math::{PixelBox, PixelSideOffsets},
-    prelude::*,
-};
+use kahlo::{math::PixelBox, prelude::*};
 
 use crate::{
     component::Component,
-    layout::{HorizontalAlignment, LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
+    layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
     theme::Theme,
     ui::UIHandle,
     window::RenderFormat,
@@ -25,7 +22,7 @@ pub struct Label<C: Component> {
 impl<C: Component> Label<C> {
     pub fn new(uih: &UIHandle) -> Self {
         let mut node = uih.new_layout_node();
-        node.set_width_policy(SizePolicy::expanding(1))
+        node.set_width_policy(SizePolicy::expanding(0))
             .set_height_policy(SizePolicy::fixed(uih.theme().ui_font_size.ceil() as usize));
         Self {
             lnode: LeafLayoutNode::new(node),
@@ -56,8 +53,14 @@ impl<C: Component> Label<C> {
         };
         let line = uih.theme().make_line(text.as_str());
         let rendered = line.render_line();
+        let sz = rendered.size();
         *self.rendered.borrow_mut() = Some(rendered);
         self.lnode.render_needed();
+
+        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);
     }
 }
 
@@ -91,23 +94,6 @@ impl<C: Component> Widget<C> for Label<C> {
         };
         let render_location = self.layout_node().render_area().unwrap();
 
-        // 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))
-            }
-        };
-
         surface.fill_region_masked(
             &rendered,
             PixelBox::from_size(rendered.size()),