Browse Source

Add simple table layout.

Kestrel 4 months ago
parent
commit
38538c60a1
12 changed files with 304 additions and 95 deletions
  1. 3 0
      Cargo.toml
  2. 7 2
      examples/table_layout.rs
  3. 27 4
      src/layout.rs
  4. 9 7
      src/layout/arr.rs
  5. 16 8
      src/layout/arr/line.rs
  6. 96 18
      src/layout/arr/table.rs
  7. 3 2
      src/layout/cache.rs
  8. 65 11
      src/layout/calc.rs
  9. 1 1
      src/lib.rs
  10. 16 42
      src/widget.rs
  11. 6 0
      src/widget/label.rs
  12. 55 0
      src/widget/spacer.rs

+ 3 - 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 layout 
+euclid = { version = "0.22" }
+
 # for rendering
 softbuffer = "0.4.2"
 kahlo = { git = "https://git.flying-kestrel.ca/kestrel/kahlo-rs.git", rev = "69001b5" }

+ 7 - 2
examples/table_layout.rs

@@ -1,4 +1,5 @@
 use patina::{
+    layout::TableCell,
     prelude::*,
     ui::{UIControlMsg, UIHandle},
     widget,
@@ -13,8 +14,12 @@ impl TableWindow {
         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)),
+                Box::new(
+                    widget::Label::new_with_text(uih, "r1c1").with_table_cell(TableCell::new(0, 0)),
+                ) as Box<dyn Widget<Self>>,
+                Box::new(
+                    widget::Label::new_with_text(uih, "r2c1").with_table_cell(TableCell::new(0, 1)),
+                ),
             ]
             .into_iter(),
         );

+ 27 - 4
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::{collections::HashMap, ops::Deref, rc::Rc};
+use std::{ops::Deref, rc::Rc};
 
 use cache::{Layer, LayoutCacheKey, NodeState};
 use kahlo::math::{PixelBox, PixelSideOffsets};
@@ -18,6 +18,10 @@ mod calc;
 pub use cache::LayoutCache;
 pub use calc::recalculate;
 
+pub struct CellMarker;
+pub type TableCell = euclid::Point2D<usize, CellMarker>;
+pub type TableSize = euclid::Size2D<usize, CellMarker>;
+
 /// Sizing policy for a layout dimension. Defaults to no minimum or desired size and a low-weighted
 /// desire to take up slack space.
 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
@@ -43,6 +47,14 @@ impl Default for SizePolicy {
 }
 
 impl SizePolicy {
+    pub fn zero() -> Self {
+        Self {
+            minimum: 0,
+            desired: 0,
+            slack_weight: 0,
+        }
+    }
+
     pub fn expanding(weight: usize) -> Self {
         Self {
             minimum: 0,
@@ -179,7 +191,7 @@ pub struct LayoutNode {
     /// 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>>,
+    table_cell: Option<TableCell>,
 }
 
 impl std::fmt::Debug for LayoutNode {
@@ -211,10 +223,14 @@ impl LayoutNode {
             halign: HorizontalAlignment::default(),
             valign: VerticalAlignment::default(),
             margin: PixelSideOffsets::new_all_same(0),
-            table_cells: None,
+            table_cell: None,
         }
     }
 
+    pub fn cache(&self) -> &Rc<LayoutCache> {
+        &self.cache
+    }
+
     pub fn relayout(&self) {
         self.cache.update_queue.borrow_mut().push(self.cache_key);
     }
@@ -319,6 +335,13 @@ impl LayoutNode {
     pub fn margin_mut(&mut self) -> &mut PixelSideOffsets {
         &mut self.margin
     }
+
+    pub fn table_cell(&self) -> &Option<TableCell> {
+        &self.table_cell
+    }
+    pub fn set_table_cell(&mut self, cell: Option<TableCell>) {
+        self.table_cell = cell;
+    }
 }
 
 impl Clone for LayoutNode {
@@ -334,7 +357,7 @@ impl Clone for LayoutNode {
             halign: self.halign,
             valign: self.valign,
             margin: self.margin,
-            table_cells: self.table_cells.clone(),
+            table_cell: self.table_cell,
         }
     }
 }

+ 9 - 7
src/layout/arr.rs

@@ -1,14 +1,16 @@
-use kahlo::math::{PixelBox, PixelPoint, PixelSize};
+use kahlo::math::PixelBox;
 use std::ops::Add;
 
-use super::{cache::NodeState, LayoutNodeAccess, SizePolicy};
+use super::{LayoutNodeAccess, SizePolicy};
 
+/// Child arrangement calculator trait.
+///
+/// Layout happens in two steps: the arrange step, and the layout step. The arrangement step
+/// calculates constraints (e.g. minimum sizes) and computes general size policies for each layout
+/// 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,
-        child_policies: Vec<(SizePolicy, SizePolicy)>,
-    ) -> (SizePolicy, SizePolicy);
+    fn arrange_step(&self, node: LayoutNodeAccess) -> (SizePolicy, SizePolicy);
     fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox);
 }
 

+ 16 - 8
src/layout/arr/line.rs

@@ -1,6 +1,9 @@
 use super::{do_fit, ArrangementCalculator};
 use crate::{
-    layout::{cache::NodeState, LayoutNodeAccess, SizePolicy},
+    layout::{
+        cache::{LayoutCacheKey, NodeState},
+        LayoutNodeAccess, SizePolicy,
+    },
     math::{PixelBox, PixelPoint, PixelSize},
 };
 use std::ops::Add;
@@ -21,15 +24,20 @@ impl LineArrangement {
 }
 
 impl ArrangementCalculator for LineArrangement {
-    fn arrange_step(
-        &self,
-        node: LayoutNodeAccess,
-        child_policies: Vec<(SizePolicy, SizePolicy)>,
-    ) -> (SizePolicy, SizePolicy) {
-        if child_policies.is_empty() {
+    fn arrange_step(&self, node: LayoutNodeAccess) -> (SizePolicy, SizePolicy) {
+        if node.child_len() == 0 {
             return (node.width_policy, node.height_policy);
         }
 
+        let child_policies = node
+            .child_iter()
+            .map(|child| {
+                node.cache
+                    .with_state(child.cache_key, |s| s.net_policy)
+                    .unwrap()
+            })
+            .collect::<Vec<_>>();
+
         let cw_policy = child_policies
             .iter()
             .map(|v| v.0)
@@ -116,7 +124,7 @@ impl ArrangementCalculator for LineArrangement {
                 )
             };
 
-            self.layout_step(child, cbox);
+            child.child_arrangement.layout_step(child, cbox);
             last_offset = offset;
         }
     }

+ 96 - 18
src/layout/arr/table.rs

@@ -1,6 +1,9 @@
 use super::{do_fit, ArrangementCalculator};
 use crate::{
-    layout::{cache::NodeState, LayoutNodeAccess, SizePolicy},
+    layout::{
+        cache::{LayoutCacheKey, NodeState},
+        LayoutNodeAccess, SizePolicy, TableCell, TableSize,
+    },
     math::{PixelBox, PixelPoint, PixelSize},
 };
 use std::ops::Add;
@@ -8,30 +11,105 @@ use std::ops::Add;
 #[derive(Clone, Debug)]
 pub struct TableArrangement;
 
+#[derive(Default)]
+struct TableState {
+    row_policies: Vec<SizePolicy>,
+    col_policies: Vec<SizePolicy>,
+}
+
 impl TableArrangement {
-    fn max_coord(&self, node: &LayoutNodeAccess) -> Option<(usize, usize)> {
-        let Some(tc) = &node.table_cells else {
-            return None;
+    fn table_size(&self, node: &LayoutNodeAccess) -> Option<TableSize> {
+        let mut max_point = Some(TableCell::zero());
+        for ch in node.child_iter() {
+            if let Some(cell) = ch.table_cell() {
+                let mp = max_point.unwrap_or_default();
+                max_point = Some(TableCell::new(mp.x.max(cell.x), mp.y.max(cell.y)));
+            }
+        }
+        max_point.and_then(|p| Some(TableSize::new(p.x + 1, p.y + 1)))
+    }
+
+    fn build_table_state(&self, node: &LayoutNodeAccess) -> TableState {
+        let mut tstate = TableState {
+            row_policies: vec![],
+            col_policies: vec![],
         };
-        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));
+
+        // pull row/column information from child node properties
+        for ch in node.child_iter() {
+            let Some(cell) = ch.table_cell() else {
+                continue;
+            };
+
+            if cell.x >= tstate.col_policies.len() {
+                tstate
+                    .col_policies
+                    .resize(cell.x + 1, SizePolicy::default());
+            }
+            if cell.y >= tstate.row_policies.len() {
+                tstate
+                    .row_policies
+                    .resize(cell.y + 1, SizePolicy::default());
+            }
+
+            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);
         }
-        max_row.zip(max_col)
+
+        tstate
     }
 }
 
 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 arrange_step(&self, node: LayoutNodeAccess) -> (SizePolicy, SizePolicy) {
+        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());
+        }
+
+        let net_rows = tstate
+            .row_policies
+            .into_iter()
+            .reduce(SizePolicy::add)
+            .unwrap();
+        let net_cols = tstate
+            .col_policies
+            .into_iter()
+            .reduce(SizePolicy::add)
+            .unwrap();
+
+        (
+            node.width_policy.max_preserve_slack(net_cols),
+            node.height_policy.max_preserve_slack(net_rows),
+        )
     }
 
-    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {}
+    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {
+        let tstate = self.build_table_state(&node);
+
+        let mut col_offsets = vec![0];
+        col_offsets.extend(do_fit(inside.width(), tstate.col_policies.into_iter()));
+        let mut row_offsets = vec![0];
+        row_offsets.extend(do_fit(inside.height(), tstate.row_policies.into_iter()));
+
+        for ch in node.child_iter() {
+            let Some(cell) = ch.table_cell() else {
+                continue;
+            };
+
+            let cbox = PixelBox::new(
+                PixelPoint::new(col_offsets[cell.x], row_offsets[cell.y]),
+                PixelPoint::new(col_offsets[cell.x + 1], row_offsets[cell.y + 1]),
+            );
+
+            ch.child_arrangement.layout_step(ch, cbox);
+        }
+    }
 }

+ 3 - 2
src/layout/cache.rs

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

+ 65 - 11
src/layout/calc.rs

@@ -28,16 +28,7 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, laye
     }
 
     // construct net size policy
-    let child_policies = node
-        .child_iter()
-        .map(|child| {
-            node.cache
-                .with_state(child.cache_key, |s| s.net_policy)
-                .unwrap()
-        })
-        .collect::<Vec<_>>();
-
-    let (wpol, hpol) = node.child_arrangement.arrange_step(node, child_policies);
+    let (wpol, hpol) = 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) {
@@ -56,6 +47,7 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, laye
                 area: None,
                 layer,
                 children: child_ids,
+                layout_state: None,
             },
         );
     }
@@ -67,7 +59,7 @@ mod test {
 
     use crate::layout::{
         cache::LayoutCache, dump_node_tree, ChildArrangement, LayoutNode, LayoutNodeAccess,
-        LayoutNodeContainer, NodeBehaviour, SizePolicy,
+        LayoutNodeContainer, NodeBehaviour, SizePolicy, TableCell,
     };
 
     use super::recalculate;
@@ -304,4 +296,66 @@ mod test {
 
         check_render_flag(&LayoutNodeAccess::new(&root));
     }
+
+    #[test]
+    fn table_test() {
+        let cache = LayoutCache::new();
+        let make_node = |tc, children| {
+            let mut lt = LayoutTree {
+                children,
+                node: LayoutNode::new(cache.clone()),
+            };
+            lt.node.set_width_policy(SizePolicy {
+                minimum: 1,
+                desired: 1,
+                slack_weight: 1,
+            });
+            lt.node.set_height_policy(SizePolicy {
+                minimum: 1,
+                desired: 1,
+                slack_weight: 1,
+            });
+            lt.node.table_cell = Some(tc);
+            lt
+        };
+
+        let mut root = LayoutTree {
+            children: vec![
+                make_node(TableCell::new(0, 0), vec![]),
+                make_node(TableCell::new(1, 1), vec![]),
+                make_node(TableCell::new(2, 2), vec![]),
+                make_node(TableCell::new(3, 3), vec![]),
+                make_node(TableCell::new(4, 4), vec![]),
+            ],
+            node: {
+                let mut ln = LayoutNode::new(cache.clone());
+                ln.child_arrangement = ChildArrangement::Table;
+                ln
+            },
+        };
+
+        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}");
+
+        recalculate(LayoutNodeAccess::new(&root));
+
+        let mut dump = String::new();
+        dump_node_tree(LayoutNodeAccess::new(&root), &mut dump);
+        println!("after recalculate:\n{dump}");
+
+        assert_eq!(
+            cache
+                .with_state(root.children[0].node.cache_key, |ns| ns.area)
+                .flatten(),
+            Some(PixelBox::from_origin_and_size(
+                PixelPoint::new(0, 0),
+                PixelSize::new(8, 8)
+            ))
+        );
+    }
 }

+ 1 - 1
src/lib.rs

@@ -9,7 +9,7 @@ pub mod window;
 
 pub mod prelude {
     pub use crate::component::Component;
-    pub use crate::widget::Widget;
+    pub use crate::widget::{Widget, WidgetExt};
     pub use crate::window::WindowComponent;
 }
 

+ 16 - 42
src/widget.rs

@@ -1,11 +1,7 @@
-use std::ops::DerefMut;
-
-use kahlo::prelude::*;
-
 use crate::{
     component::Component,
     input::InputState,
-    layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
+    layout::{LayoutNode, LayoutNodeAccess, TableCell},
     theme::Theme,
     ui::UIHandle,
     window::RenderFormat,
@@ -19,6 +15,8 @@ mod group;
 pub use group::PlainGroup;
 mod button;
 pub use button::Button;
+mod spacer;
+pub use spacer::Spacer;
 
 pub trait Widget<C: Component> {
     fn poll(&mut self, uih: &mut UIHandle, input_state: Option<&InputState>) -> Vec<C::Msg>;
@@ -28,44 +26,20 @@ pub trait Widget<C: Component> {
     fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>);
 }
 
-pub struct Spacer<C: Component> {
-    lnode: LeafLayoutNode,
-    _ghost: std::marker::PhantomData<C>,
-}
-
-impl<C: Component> Spacer<C> {
-    pub fn new(uih: &UIHandle) -> Self {
-        let mut lnode = LeafLayoutNode::new(uih.new_layout_node());
-        lnode
-            .set_width_policy(SizePolicy::expanding(1))
-            .set_height_policy(SizePolicy::expanding(1));
-        Self {
-            lnode,
-            _ghost: Default::default(),
-        }
+pub trait WidgetExt<C: Component>: Widget<C> {
+    fn clear_table_cell(&mut self) {
+        self.layout_node_mut().set_table_cell(None);
     }
-}
-
-impl<C: Component> Widget<C> for Spacer<C> {
-    fn poll(
-        &mut self,
-        _uih: &mut UIHandle,
-        _input_state: Option<&InputState>,
-    ) -> Vec<<C as Component>::Msg> {
-        vec![]
+    fn set_table_cell(&mut self, cell: TableCell) {
+        self.layout_node_mut().set_table_cell(Some(cell));
     }
-
-    fn layout_node(&self) -> LayoutNodeAccess {
-        LayoutNodeAccess::new(&self.lnode)
-    }
-    fn layout_node_mut(&mut self) -> &mut LayoutNode {
-        self.lnode.deref_mut()
-    }
-
-    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {
-        let region = self.layout_node().render_area().unwrap();
-        if self.lnode.render_check() {
-            target.fill_region(region, theme.background);
-        }
+    fn with_table_cell(mut self, cell: TableCell) -> Self
+    where
+        Self: Sized,
+    {
+        self.set_table_cell(cell);
+        self
     }
 }
+
+impl<C: Component, W: Widget<C>> WidgetExt<C> for W {}

+ 6 - 0
src/widget/label.rs

@@ -35,6 +35,12 @@ impl<C: Component> Label<C> {
         }
     }
 
+    pub fn new_with_text(uih: &UIHandle, text: &str) -> Self {
+        let mut r = Self::new(uih);
+        r.set_text(text);
+        r
+    }
+
     pub fn set_text(&mut self, text: &str) {
         if Some(text) != self.text.as_deref() {
             self.text = Some(text.to_string());

+ 55 - 0
src/widget/spacer.rs

@@ -0,0 +1,55 @@
+use std::ops::DerefMut;
+
+use kahlo::prelude::*;
+
+use crate::{
+    component::Component,
+    input::InputState,
+    layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode, SizePolicy},
+    theme::Theme,
+    ui::UIHandle,
+    widget::Widget,
+    window::RenderFormat,
+};
+
+pub struct Spacer<C: Component> {
+    lnode: LeafLayoutNode,
+    _ghost: std::marker::PhantomData<C>,
+}
+
+impl<C: Component> Spacer<C> {
+    pub fn new(uih: &UIHandle) -> Self {
+        let mut lnode = LeafLayoutNode::new(uih.new_layout_node());
+        lnode
+            .set_width_policy(SizePolicy::expanding(1))
+            .set_height_policy(SizePolicy::expanding(1));
+        Self {
+            lnode,
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<C: Component> Widget<C> for Spacer<C> {
+    fn poll(
+        &mut self,
+        _uih: &mut UIHandle,
+        _input_state: Option<&InputState>,
+    ) -> Vec<<C as Component>::Msg> {
+        vec![]
+    }
+
+    fn layout_node(&self) -> LayoutNodeAccess {
+        LayoutNodeAccess::new(&self.lnode)
+    }
+    fn layout_node_mut(&mut self) -> &mut LayoutNode {
+        self.lnode.deref_mut()
+    }
+
+    fn render(&self, theme: &Theme, target: &mut kahlo::BitmapMut<RenderFormat>) {
+        let region = self.layout_node().render_area().unwrap();
+        if self.lnode.render_check() {
+            target.fill_region(region, theme.background);
+        }
+    }
+}