Parcourir la source

Ergonomic improvements.

Kestrel il y a 5 mois
Parent
commit
a992628098

+ 38 - 0
patina-sdl/src/convert.rs

@@ -0,0 +1,38 @@
+use patina::platform::render as pr;
+use patina::geom::*;
+
+pub trait ToSDL {
+    type SDLEquivalent;
+    fn to_sdl(self) -> Self::SDLEquivalent;
+}
+
+impl ToSDL for pr::Colour {
+    type SDLEquivalent = sdl2::pixels::Color;
+    fn to_sdl(self) -> Self::SDLEquivalent {
+        sdl2::pixels::Color::RGBA(self.r(), self.g(), self.b(), self.a())
+    }
+}
+
+impl ToSDL for IRect {
+    type SDLEquivalent = sdl2::rect::Rect;
+    fn to_sdl(self) -> Self::SDLEquivalent {
+        sdl2::rect::Rect::new(
+            self.base().x,
+            self.base().y,
+            self.size().x as u32,
+            self.size().y as u32,
+        )
+    }
+}
+
+pub trait FromSDL {
+    type PatinaEquivalent;
+    fn from_sdl(self) -> Self::PatinaEquivalent;
+}
+
+impl FromSDL for sdl2::rect::Rect {
+    type PatinaEquivalent = IRect;
+    fn from_sdl(self) -> Self::PatinaEquivalent {
+        IRect::new_from_size(IPoint { x: self.x, y: self.y }, IVector { x: self.w, y: self.h })
+    }
+}

+ 51 - 13
patina-sdl/src/lib.rs

@@ -1,5 +1,12 @@
 use patina::platform;
 
+mod convert;
+mod render;
+
+mod prelude {
+    pub use crate::convert::{FromSDL,ToSDL};
+}
+
 pub struct SDLInitFlags {
     pub window_width: usize,
     pub window_height: usize,
@@ -19,7 +26,8 @@ impl Default for SDLInitFlags {
 pub struct SDLRenderInterface {
     sdl: sdl2::Sdl,
     video: sdl2::VideoSubsystem,
-    window: sdl2::video::Window,
+    // window: sdl2::video::Window,
+    canvas: render::WindowSurface,
 }
 
 impl SDLRenderInterface {
@@ -38,11 +46,25 @@ impl SDLRenderInterface {
             .build()
             .unwrap();
 
-        (Self { sdl, video, window }, SDLEventInterface { evt, next: None })
+        let canvas = window.into_canvas().present_vsync().build().unwrap();
+
+        (
+            Self {
+                sdl,
+                video,
+                canvas: canvas.into(),
+            },
+            SDLEventInterface { evt, next: None },
+        )
     }
 }
 
-impl platform::RenderInterface for SDLRenderInterface {}
+impl platform::RenderInterface for SDLRenderInterface {
+    type RenderTarget = render::WindowSurface;
+    fn render_target_mut(&mut self) -> &mut Self::RenderTarget {
+        &mut self.canvas
+    }
+}
 
 pub struct SDLEventInterface {
     evt: sdl2::EventPump,
@@ -56,30 +78,46 @@ impl SDLEventInterface {
             _ => None,
         }
     }
+
+    fn poll_internal(&mut self) -> Option<sdl2::event::Event> {
+        self.next.take().or_else(|| self.evt.poll_event())
+    }
 }
 
 impl platform::EventInterface for SDLEventInterface {
     fn poll_event(&mut self) -> Option<platform::event::WindowEvent> {
-        if let Some(evt) = self.next.take() {
-            self.translate(evt)
-        } else {
-            let sdlevent = self.evt.poll_event()?;
-            self.translate(sdlevent)
+        while let Some(evt) = self.poll_internal() {
+            if let Some(wevt) = self.translate(evt) {
+                return Some(wevt);
+            }
         }
+        None
     }
-    fn wait_event(&mut self) {
-        if self.next.is_some() { return }
+    fn wait_for_event(&mut self) {
+        if self.next.is_some() {
+            return;
+        }
         self.next = Some(self.evt.wait_event());
     }
 }
 
-pub struct SDLTextInterface {}
+pub struct SDLTextInterface {
+    ctx: sdl2::ttf::Sdl2TtfContext,
+}
 
-impl platform::TextInterface for SDLTextInterface {}
+impl platform::TextInterface for SDLTextInterface {
+    fn render_text(&self, what: &str, _sz: f32) -> Box<dyn patina::platform::render::Surface + '_> {
+        let font = self.ctx.load_font("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16).unwrap();
+        let render = font.render(what);
+        let rendered = render.solid(sdl2::pixels::Color::RGB(255, 255, 255)).unwrap();
+        Box::new(render::SDLSurface { surface: rendered })
+    }
+}
 
 pub fn init_sdl_platform(
     sdlif: SDLInitFlags,
 ) -> (SDLRenderInterface, SDLEventInterface, SDLTextInterface) {
     let (sdl, event) = SDLRenderInterface::new(sdlif);
-    (sdl, event, SDLTextInterface {})
+    let ttf = SDLTextInterface { ctx: sdl2::ttf::init().unwrap() };
+    (sdl, event, ttf)
 }

+ 68 - 0
patina-sdl/src/render.rs

@@ -0,0 +1,68 @@
+use crate::prelude::*;
+
+use patina::geom::{IPoint, IRect, IVector};
+use patina::platform::render as pr;
+
+pub struct SDLSurface<'l> {
+    pub surface: sdl2::surface::Surface<'l>,
+}
+
+impl<'l> pr::Surface for SDLSurface<'l> {
+    fn size(&self) -> IVector {
+        self.surface.rect().from_sdl().size()
+    }
+
+    fn painter(&mut self) -> Option<Box<dyn pr::Painter + '_>> {
+        None
+    }
+}
+
+pub struct WindowSurface {
+    canvas: sdl2::render::WindowCanvas,
+}
+
+impl From<sdl2::render::WindowCanvas> for WindowSurface {
+    fn from(canvas: sdl2::render::WindowCanvas) -> Self {
+        Self { canvas }
+    }
+}
+
+impl pr::Surface for WindowSurface {
+    fn size(&self) -> IVector {
+        let sz = self.canvas.window().size();
+        IVector {
+            x: sz.0 as i32,
+            y: sz.1 as i32,
+        }
+    }
+
+    fn painter(&mut self) -> Option<Box<dyn pr::Painter + '_>> {
+        Some(Box::new(CanvasPainter {
+            canvas: &mut self.canvas,
+            _ghost: Default::default(),
+        }))
+    }
+}
+
+pub struct CanvasPainter<'l> {
+    canvas: &'l mut sdl2::render::WindowCanvas,
+    _ghost: std::marker::PhantomData<(&'l (),)>,
+}
+
+impl<'l> pr::Painter for CanvasPainter<'l> {
+    fn blit(&mut self, source: &dyn pr::Surface, source_area: IRect, target: IPoint) {
+        // self.canvas.copy
+        // self.canvas.copy(source)
+    }
+    fn fill(&mut self, area: IRect, col: pr::Colour) {
+        self.canvas.set_draw_color(col.to_sdl());
+        self.canvas.fill_rect(area.to_sdl()).expect("couldn't draw");
+    }
+    fn outline_rect(&mut self, rect: IRect, col: pr::Colour) {}
+}
+
+impl<'l> Drop for CanvasPainter<'l> {
+    fn drop(&mut self) {
+        self.canvas.present()
+    }
+}

+ 68 - 6
patina/examples/sdl_plain.rs

@@ -1,10 +1,72 @@
-use patina::prelude::*;
-// use patina_sdl::
+// use patina::prelude::*;
 
-fn main() {
-    let ui = patina::UI::new(patina_sdl::init_sdl_platform(patina_sdl::SDLInitFlags {
-        ..Default::default()
-    }));
+use patina::{
+    layout::SizePolicy,
+    widget::Widget,
+};
+
+struct SimpleRoot {
+    frame: patina::widget::Frame<Self>,
+}
+
+#[derive(Debug)]
+enum SimpleRootMsg {
+    CloseRequest,
+}
+
+impl patina::component::Component for SimpleRoot {
+    type ParentMsg = patina::UIControlMsg;
+    type Msg = SimpleRootMsg;
+
+    fn process(&mut self, msg: Self::Msg) -> Vec<Self::ParentMsg> {
+        match msg {
+            SimpleRootMsg::CloseRequest => vec![patina::UIControlMsg::Terminate],
+        }
+    }
+}
+
+impl patina::UIComponent for SimpleRoot {
+    fn build(bc: &patina::widget::BuildContext) -> Self {
+        let mut frame = patina::widget::Frame::<Self>::new(bc);
+        frame
+            .layout_node_mut()
+            .set_width_policy(SizePolicy {
+                minimum: 100,
+                desired: 200,
+                slack_weight: 1,
+            })
+            .set_height_policy(SizePolicy {
+                minimum: 100,
+                desired: 200,
+                slack_weight: 1,
+            })
+            .set_label("frame");
+
+        let mut label = patina::widget::Label::<Self>::new(bc);
+        label.layout_node_mut().set_height_policy(SizePolicy { minimum: 20, desired: 20, slack_weight: 0 }).set_width_policy(SizePolicy { minimum: 10, desired: 100, slack_weight: 1 }).set_label("label");
+        frame.set_child(Box::new(label));
+
+        Self { frame }
+    }
 
+    fn map_ui_event(&self, uievent: patina::platform::event::UIEvent) -> Option<Self::Msg> {
+        match uievent {
+            patina::platform::event::UIEvent::CloseRequest => Some(SimpleRootMsg::CloseRequest),
+            _ => None,
+        }
+    }
+    type RootWidget = patina::widget::Frame<Self>;
+    fn root_widget(&self) -> &Self::RootWidget {
+        &self.frame
+    }
+    fn root_widget_mut(&mut self) -> &mut Self::RootWidget {
+        &mut self.frame
+    }
+}
+
+fn main() {
+    let ui = patina::UI::<_, SimpleRoot>::new(patina_sdl::init_sdl_platform(
+        patina_sdl::SDLInitFlags::default(),
+    ));
     ui.run();
 }

+ 9 - 18
patina/src/component.rs

@@ -7,7 +7,7 @@ pub struct ComponentStorage<Msg> {
 impl<Msg> Default for ComponentStorage<Msg> {
     fn default() -> Self {
         Self {
-            queue: vec![].into()
+            queue: vec![].into(),
         }
     }
 }
@@ -22,8 +22,11 @@ pub trait Component: 'static {
     type ParentMsg;
     type Msg;
 
-    fn storage(&self) -> Rc<ComponentStorage<Self::Msg>>;
-    fn process(&mut self, msg: Self::Msg) -> Option<Self::ParentMsg>;
+    fn process(&mut self, msg: Self::Msg) -> Vec<Self::ParentMsg>;
+
+    fn process_all(&mut self, msgs: impl Iterator<Item = Self::Msg>) -> Vec<Self::ParentMsg> {
+        msgs.map(|msg| self.process(msg)).flatten().collect()
+    }
 }
 
 /*
@@ -89,9 +92,7 @@ pub mod counter_component_example {
         type ParentMsg = PC::Msg;
         type Msg = CounterMsg;
 
-        fn storage(&self) -> Rc<ComponentStorage<Self::Msg>> { self.cstore.clone() }
-
-        fn process(&mut self, msg: Self::Msg) -> Option<Self::ParentMsg> {
+        fn process(&mut self, msg: Self::Msg) -> Vec<Self::ParentMsg> {
             match msg {
                 CounterMsg::Increase => self.val += 1,
                 CounterMsg::Decrease => self.val -= 1,
@@ -105,18 +106,8 @@ pub mod counter_component_example {
         fn render(&self, surface: ()) { todo!() }
         fn layout_node(&self) -> &dyn crate::layout::LayoutNodeAccess { todo!() }
 
-        fn poll(&mut self, input_state: &crate::input::InputState) -> impl Iterator<Item = <PC as Component>::Msg> {
-            self.cstore.queue.borrow_mut().extend(self.inc.poll(input_state).chain(self.dec.poll(input_state)));
-
-            let mut pb = vec![];
-
-            loop {
-                let Some(v) = self.cstore.queue.borrow_mut().pop() else { break };
-
-                pb.extend(self.process(v).into_iter());
-            }
-
-            pb.into_iter()
+        fn poll(&mut self, input_state: &crate::input::InputState) -> Vec<PC::Msg> {
+            self.process_all(self.inc.poll(input_state).chain(self.dec.poll(input_state)))
         }
     }
 }

+ 6 - 0
patina/src/geom.rs

@@ -63,6 +63,12 @@ impl<GF: GeomField> Point<GF> {
     };
 }
 
+impl<GF: GeomField> Default for Point<GF> {
+    fn default() -> Self {
+        Self::ORIGIN
+    }
+}
+
 impl<GF: GeomField> std::ops::Add<Vector<GF>> for Point<GF> {
     type Output = Self;
     fn add(self, rhs: Vector<GF>) -> Self::Output {

+ 50 - 1
patina/src/input.rs

@@ -1 +1,50 @@
-pub struct InputState;
+use crate::geom::{IPoint, IVector};
+
+mod button;
+pub use button::ButtonSet;
+
+#[derive(Clone, Copy, Debug)]
+#[repr(u8)]
+pub enum MouseButton {
+    Left,
+    Right,
+    Middle,
+    ScrollUp,
+    ScrollDown,
+    ExtraButton1,
+    ExtraButton2,
+    ExtraButton3,
+    ExtraButton4,
+}
+
+impl Into<u8> for MouseButton {
+    fn into(self) -> u8 {
+        self as u8
+    }
+}
+
+#[derive(Default)]
+pub struct MouseState {
+    pub pos: IPoint,
+    pub last_pos: IPoint,
+    pub buttons: ButtonSet<MouseButton>,
+    pub last_buttons: ButtonSet<MouseButton>,
+}
+
+impl MouseState {
+    pub fn shift(&self) -> IVector {
+        self.pos - self.last_pos
+    }
+
+    pub fn pressed(&self) -> ButtonSet<MouseButton> {
+        self.buttons & !self.last_buttons
+    }
+    pub fn released(&self) -> ButtonSet<MouseButton> {
+        !self.buttons & self.last_buttons
+    }
+}
+
+#[derive(Default)]
+pub struct InputState {
+    pub mouse: MouseState,
+}

+ 41 - 0
patina/src/input/button.rs

@@ -0,0 +1,41 @@
+pub trait ButtonSpec: Copy + Into<u8> {}
+impl<T: Copy + Into<u8>> ButtonSpec for T {}
+
+#[derive(Clone, Copy, Debug)]
+pub struct ButtonSet<BS: ButtonSpec>(u64, std::marker::PhantomData<BS>);
+
+impl<BS: ButtonSpec> ButtonSet<BS> {
+    pub fn active(&self, button: BS) -> bool {
+        (self.0 >> (button.into())) & 1 == 1
+    }
+    pub fn inactive(&self, button: BS) -> bool {
+        !self.active(button)
+    }
+}
+
+impl<BS: ButtonSpec> Default for ButtonSet<BS> {
+    fn default() -> Self {
+        Self(0, Default::default())
+    }
+}
+
+impl<BS: ButtonSpec> std::ops::BitAnd for ButtonSet<BS> {
+    type Output = Self;
+    fn bitand(self, rhs: Self) -> Self::Output {
+        Self(self.0 & rhs.0, Default::default())
+    }
+}
+
+impl<BS: ButtonSpec> std::ops::BitOr for ButtonSet<BS> {
+    type Output = Self;
+    fn bitor(self, rhs: Self) -> Self::Output {
+        Self(self.0 | rhs.0, Default::default())
+    }
+}
+
+impl<BS: ButtonSpec> std::ops::Not for ButtonSet<BS> {
+    type Output = Self;
+    fn not(self) -> Self::Output {
+        Self(!self.0, Default::default())
+    }
+}

+ 171 - 20
patina/src/layout.rs

@@ -1,7 +1,4 @@
-use std::{
-    ops::{Deref, DerefMut},
-    rc::Rc,
-};
+use std::{ops::Deref, rc::Rc};
 
 use crate::geom::IRect;
 use cache::{Layer, LayoutCacheKey, NodeState};
@@ -123,8 +120,8 @@ impl Deref for ChildArrangement {
     }
 }
 
-#[derive(Debug)]
 pub struct LayoutNode {
+    label: Option<String>,
     cache_key: LayoutCacheKey,
     cache: Rc<LayoutCache>,
     behaviour: NodeBehaviour,
@@ -136,11 +133,27 @@ pub struct LayoutNode {
     margin: BoxMeasure,
 }
 
+impl std::fmt::Debug for LayoutNode {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("LayoutNode")
+            .field("label", &self.label)
+            .field("cache_key", &self.cache_key)
+            .field("behaviour", &self.behaviour)
+            .field("child_arrangement", &self.child_arrangement)
+            .field("width_policy", &self.width_policy)
+            .field("height_policy", &self.height_policy)
+            .field("padding", &self.padding)
+            .field("margin", &self.margin)
+            .finish()
+    }
+}
+
 impl LayoutNode {
     pub fn new(cache: Rc<LayoutCache>) -> Self {
         let cache_key = LayoutCacheKey::generate();
         cache.update_queue.borrow_mut().push(cache_key);
         Self {
+            label: None,
             cache_key,
             cache,
             behaviour: NodeBehaviour::Element,
@@ -155,39 +168,56 @@ impl LayoutNode {
     pub fn relayout(&self) {
         self.cache.update_queue.borrow_mut().push(self.cache_key);
     }
+
+    pub fn cached_rect(&self) -> Option<IRect> {
+        self.cache
+            .with_state(self.cache_key, |ns| ns.rect)
+            .flatten()
+    }
 }
 
 // accessors
 impl LayoutNode {
+    pub fn label(&self) -> Option<&str> {
+        self.label.as_ref().map(String::as_str)
+    }
+    pub fn set_label(&mut self, to: impl AsRef<str>) {
+        self.label = Some(to.as_ref().to_string());
+    }
+
     pub fn behaviour(&self) -> NodeBehaviour {
         self.behaviour
     }
-    pub fn set_behaviour(&mut self, mode: NodeBehaviour) {
+    pub fn set_behaviour(&mut self, mode: NodeBehaviour) -> &mut Self {
         self.behaviour = mode;
         self.relayout();
+        self
     }
 
     pub fn arrangement(&self) -> &ChildArrangement {
         &self.child_arrangement
     }
-    pub fn set_arrangement(&mut self, arr: ChildArrangement) {
+    pub fn set_arrangement(&mut self, arr: ChildArrangement) -> &mut Self {
         self.child_arrangement = arr;
         self.relayout();
+        self
     }
 
     pub fn width_policy(&self) -> SizePolicy {
         self.width_policy
     }
-    pub fn set_width_policy(&mut self, policy: SizePolicy) {
+    pub fn set_width_policy(&mut self, policy: SizePolicy) -> &mut Self {
         self.width_policy = policy;
         self.relayout();
+        self
     }
     pub fn height_policy(&self) -> SizePolicy {
         self.height_policy
     }
-    pub fn set_height_policy(&mut self, policy: SizePolicy) {
+    pub fn set_height_policy(&mut self, policy: SizePolicy) -> &mut Self {
         self.height_policy = policy;
         self.relayout();
+        self
     }
 
     pub fn padding(&self) -> BoxMeasure {
@@ -207,6 +237,7 @@ impl LayoutNode {
 impl Clone for LayoutNode {
     fn clone(&self) -> Self {
         Self {
+            label: self.label.clone(),
             cache_key: LayoutCacheKey::generate(),
             cache: self.cache.clone(),
             behaviour: self.behaviour,
@@ -225,35 +256,155 @@ impl Drop for LayoutNode {
     }
 }
 
+fn dump_node_tree_helper(lna: LayoutNodeAccess, indent: usize, out: &mut String) {
+    let ind = "    ".repeat(indent);
+    out.push_str(ind.as_str());
+    out.push_str("Node (");
+    out.push_str(match lna.label() {
+        Some(v) => v,
+        None => "<anon>",
+    });
+    out.push_str(format!(") [wpol: {}/{}/{} hpol: {}/{}/{} behaviour: {:?}]\n",
+        lna.width_policy.minimum, lna.width_policy.desired, lna.width_policy.slack_weight,
+        lna.height_policy.minimum, lna.height_policy.desired, lna.height_policy.slack_weight,
+        lna.behaviour).as_str());
+    out.push_str(ind.as_str());
+    out.push_str("        ");
+    out.push_str(format!("cached rect: {:?}\n", lna.cached_rect()).as_str());
+
+    for child in lna.child_iter() {
+        dump_node_tree_helper(child, indent+1, out);
+    }
+}
+
+pub fn dump_node_tree(lna: LayoutNodeAccess, out: &mut String) {
+    out.clear();
+    dump_node_tree_helper(lna, 0, out);
+}
+
+/// Iterator implementation to iterate across children of a LayoutNodeContainer
 #[derive(Clone)]
 pub struct LayoutChildIter<'l> {
-    lna: &'l dyn LayoutNodeAccess,
+    lnc: &'l dyn LayoutNodeContainer,
     next_index: usize,
 }
 
 impl<'l> LayoutChildIter<'l> {
-    pub fn new(lna: &'l dyn LayoutNodeAccess) -> Self {
-        Self { lna, next_index: 0 }
+    fn new(lnc: &'l dyn LayoutNodeContainer) -> Self {
+        Self { lnc, next_index: 0 }
     }
 }
 
 impl<'l> Iterator for LayoutChildIter<'l> {
-    type Item = &'l dyn LayoutNodeAccess;
+    type Item = LayoutNodeAccess<'l>;
 
     fn next(&mut self) -> Option<Self::Item> {
         let index = self.next_index;
-        if index >= self.lna.child_count() {
+        if index >= self.lnc.layout_child_count() {
             None
         } else {
             self.next_index += 1;
-            self.lna.child(index)
+            self.lnc.layout_child(index)
         }
     }
 }
 
-/// Accessor trait for a [`LayoutNode`] tree, which has externally-stored relationships.
-pub trait LayoutNodeAccess: Deref<Target = LayoutNode> + DerefMut<Target = LayoutNode> {
-    fn child_count(&self) -> usize;
-    fn child(&self, ndx: usize) -> Option<&dyn LayoutNodeAccess>;
-    fn child_iter(&self) -> LayoutChildIter;
+#[derive(Clone, Copy)]
+pub struct LayoutNodeAccess<'l> {
+    lnc: &'l dyn LayoutNodeContainer,
+}
+
+impl<'l> LayoutNodeAccess<'l> {
+    pub fn new(lnc: &'l dyn LayoutNodeContainer) -> Self {
+        Self { lnc }
+    }
+}
+
+impl<'l> Deref for LayoutNodeAccess<'l> {
+    type Target = LayoutNode;
+    fn deref(&self) -> &Self::Target {
+        self.lnc.layout_node()
+    }
+}
+
+impl<'l> LayoutNodeAccess<'l> {
+    pub fn child(&self, ndx: usize) -> Option<LayoutNodeAccess<'l>> {
+        self.lnc.layout_child(ndx)
+    }
+    pub fn child_len(&self) -> usize {
+        self.lnc.layout_child_count()
+    }
+    pub fn child_iter(&self) -> LayoutChildIter {
+        LayoutChildIter::new(self.lnc)
+    }
+}
+
+/// Data source trait for LayoutNodeAccess.
+pub trait LayoutNodeContainer {
+    fn layout_node(&self) -> &LayoutNode;
+    fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess<'_>>;
+    fn layout_child_count(&self) -> usize;
+}
+
+/// Helper struct to store a leaf LayoutNode and automatically provide a LayoutNodeContainer impl
+pub struct LeafLayoutNode(LayoutNode);
+
+impl LeafLayoutNode {
+    pub fn new(ln: LayoutNode) -> Self {
+        Self(ln)
+    }
+}
+
+impl From<LayoutNode> for LeafLayoutNode {
+    fn from(value: LayoutNode) -> Self {
+        Self::new(value)
+    }
+}
+
+impl std::ops::Deref for LeafLayoutNode {
+    type Target = LayoutNode;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl std::ops::DerefMut for LeafLayoutNode {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.0
+    }
+}
+
+impl LayoutNodeContainer for LeafLayoutNode {
+    fn layout_node(&self) -> &LayoutNode {
+        &self.0
+    }
+    fn layout_child(&self, _ndx: usize) -> Option<LayoutNodeAccess<'_>> {
+        None
+    }
+    fn layout_child_count(&self) -> usize {
+        0
+    }
+}
+
+/// Helper LayoutNodeContainer implementation for LayoutNodes with one child
+pub struct LinearAccess<'l>(&'l LayoutNode, LayoutNodeAccess<'l>);
+impl<'l> LinearAccess<'l> {
+    pub fn new(parent: &'l LayoutNode, child: LayoutNodeAccess<'l>) -> Self {
+        Self(parent, child)
+    }
+}
+impl<'l> LayoutNodeContainer for LinearAccess<'l> {
+    fn layout_node(&self) -> &LayoutNode {
+        self.0
+    }
+    fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess> {
+        if ndx == 0 {
+            Some(self.1)
+        } else {
+            None
+        }
+    }
+    fn layout_child_count(&self) -> usize {
+        1
+    }
 }

+ 20 - 9
patina/src/layout/arr.rs

@@ -7,10 +7,10 @@ use super::{cache::NodeState, LayoutNodeAccess, SizePolicy};
 pub trait ArrangementCalculator {
     fn arrange_step(
         &self,
-        node: &dyn LayoutNodeAccess,
+        node: LayoutNodeAccess,
         child_policies: Vec<(SizePolicy, SizePolicy)>,
     ) -> (SizePolicy, SizePolicy);
-    fn layout_step(&self, node: &dyn LayoutNodeAccess, inside: IRect);
+    fn layout_step(&self, node: LayoutNodeAccess, inside: IRect);
 }
 
 #[derive(Clone, Debug)]
@@ -31,7 +31,7 @@ impl LineArrangement {
 impl ArrangementCalculator for LineArrangement {
     fn arrange_step(
         &self,
-        node: &dyn LayoutNodeAccess,
+        node: LayoutNodeAccess,
         child_policies: Vec<(SizePolicy, SizePolicy)>,
     ) -> (SizePolicy, SizePolicy) {
         if child_policies.is_empty() {
@@ -57,16 +57,20 @@ impl ArrangementCalculator for LineArrangement {
             })
             .unwrap();
         (
-            cw_policy.max_preserve_slack(node.width_policy),
-            ch_policy.max_preserve_slack(node.height_policy),
+            node.width_policy.max_preserve_slack(cw_policy),
+            node.height_policy.max_preserve_slack(ch_policy),
         )
     }
 
-    fn layout_step(&self, node: &dyn LayoutNodeAccess, inside: IRect) {
+    fn layout_step(&self, node: LayoutNodeAccess, inside: IRect) {
         // do the final children layouts
         node.cache
             .with_state(node.cache_key, |ns| ns.rect = Some(inside));
 
+        if node.child_len() == 0 {
+            return;
+        }
+
         // expansion direction extraction lambda
         let ee = if self.is_column() {
             |ns: &mut NodeState| ns.net_policy.1
@@ -125,13 +129,20 @@ fn do_fit<'l>(
     // 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();
 
+    if (policy_sum.desired - policy_sum.minimum) == 0 {}
+
     // how much of the desired size can we fit?
     let fit_coeff = (total - policy_sum.minimum as i32) as f32
         / (policy_sum.desired - policy_sum.minimum) as f32;
-    let fit_coeff = fit_coeff.min(1.0); // not more than 100% of desired
+    let fit_coeff = if fit_coeff.is_nan() {
+        1.0
+    } else {
+        // not more than 100% of desired
+        fit_coeff.min(1.0)
+    };
 
     // how much slack space do we have left?
-    let slack_coeff = if policy_sum.slack_weight > 0 && fit_coeff > 1.0 {
+    let slack_coeff = if policy_sum.slack_weight > 0 && fit_coeff >= 1.0 {
         (total - policy_sum.desired as i32) as f32 / policy_sum.slack_weight as f32
     } else {
         0.0
@@ -141,7 +152,7 @@ fn do_fit<'l>(
     // second pass over children: actually space out according to size policies and calculated coefficients
     policies.map(move |policy| {
         let mut amount = policy.minimum as f32;
-        amount += policy.desired as f32 * fit_coeff;
+        amount += (policy.desired - policy.minimum) as f32 * fit_coeff;
         amount += policy.slack_weight as f32 * slack_coeff;
         let amount = amount.round() as i32;
         offset += amount;

+ 20 - 37
patina/src/layout/calc.rs

@@ -2,7 +2,7 @@ use crate::geom::{IRect, IVector};
 
 use super::{cache::LayoutCacheKey, Layer, LayoutNode, LayoutNodeAccess, NodeBehaviour, NodeState};
 
-pub fn recalculate(node: &dyn LayoutNodeAccess) {
+pub fn recalculate(node: LayoutNodeAccess) {
     if let NodeBehaviour::Fixed { rect } = node.behaviour() {
         let layer = Layer::default();
 
@@ -11,19 +11,13 @@ pub fn recalculate(node: &dyn LayoutNodeAccess) {
 
         arrangement_pass(None, node, layer);
 
-        println!("invoking layout_step with rect {rect:?}");
-
         node.child_arrangement.layout_step(node, rect);
     } else {
         log::warn!("Layout root node does not have Fixed mode");
     }
 }
 
-fn arrangement_pass(parent: Option<LayoutCacheKey>, node: &dyn LayoutNodeAccess, layer: Layer) {
-    println!(
-        "arrangement_pass({:?}, {:?}, {:?})",
-        parent, node.cache_key, layer
-    );
+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) {
         return;
@@ -66,13 +60,11 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: &dyn LayoutNodeAccess,
 
 #[cfg(test)]
 mod test {
-    use std::ops::{Deref, DerefMut};
-
     use crate::{
         geom::{IPoint, IRect, IVector},
         layout::{
-            cache::LayoutCache, ChildArrangement, LayoutChildIter, LayoutNode, LayoutNodeAccess,
-            NodeBehaviour, SizePolicy,
+            cache::LayoutCache, ChildArrangement, LayoutNode, LayoutNodeAccess,
+            LayoutNodeContainer, NodeBehaviour, SizePolicy,
         },
     };
 
@@ -83,26 +75,14 @@ mod test {
         node: LayoutNode,
     }
 
-    impl Deref for LayoutTree {
-        type Target = LayoutNode;
-        fn deref(&self) -> &Self::Target {
+    impl LayoutNodeContainer for LayoutTree {
+        fn layout_node(&self) -> &LayoutNode {
             &self.node
         }
-    }
-    impl DerefMut for LayoutTree {
-        fn deref_mut(&mut self) -> &mut Self::Target {
-            &mut self.node
-        }
-    }
-
-    impl<'l> LayoutNodeAccess for LayoutTree {
-        fn child(&self, ndx: usize) -> Option<&dyn LayoutNodeAccess> {
-            self.children.get(ndx).map(|v| v as &dyn LayoutNodeAccess)
-        }
-        fn child_iter(&self) -> crate::layout::LayoutChildIter {
-            LayoutChildIter::new(self)
+        fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess<'_>> {
+            self.children.get(ndx).map(|t| LayoutNodeAccess::new(t))
         }
-        fn child_count(&self) -> usize {
+        fn layout_child_count(&self) -> usize {
             self.children.len()
         }
     }
@@ -118,7 +98,7 @@ mod test {
                         children: vec![],
                         node: LayoutNode::new(cache.clone()),
                     };
-                    lt.set_height_policy(SizePolicy {
+                    lt.node.set_height_policy(SizePolicy {
                         minimum: 1,
                         desired: 1,
                         slack_weight: 1,
@@ -130,7 +110,7 @@ mod test {
                         children: vec![],
                         node: LayoutNode::new(cache.clone()),
                     };
-                    lt.set_height_policy(SizePolicy {
+                    lt.node.set_height_policy(SizePolicy {
                         minimum: 2,
                         desired: 3,
                         slack_weight: 0,
@@ -140,16 +120,20 @@ mod test {
             ],
             node: LayoutNode::new(cache.clone()),
         };
-        root.set_behaviour(NodeBehaviour::Fixed {
+        root.node.set_behaviour(NodeBehaviour::Fixed {
             rect: IRect::new_from_size(IPoint { x: 1, y: 1 }, IVector { x: 2, y: 5 }),
         });
-        root.child_arrangement = ChildArrangement::Column;
+        root.node.child_arrangement = ChildArrangement::Column;
+
+        recalculate(LayoutNodeAccess::new(&root));
 
-        recalculate(&root);
+        println!("cache: {:?}", cache);
 
         // check that final rects match expectations
         assert_eq!(
-            cache.with_state(root.cache_key, |ns| ns.rect).flatten(),
+            cache
+                .with_state(root.node.cache_key, |ns| ns.rect)
+                .flatten(),
             Some(IRect::new_from_size(
                 IPoint { x: 1, y: 1 },
                 IVector { x: 2, y: 5 }
@@ -158,7 +142,7 @@ mod test {
 
         assert_eq!(
             cache
-                .with_state(root.children[0].cache_key, |ns| ns.rect)
+                .with_state(root.children[0].node.cache_key, |ns| ns.rect)
                 .flatten(),
             Some(IRect::new_from_size(
                 IPoint { x: 1, y: 1 },
@@ -166,6 +150,5 @@ mod test {
             ))
         );
 
-        println!("cache: {:?}", cache);
     }
 }

+ 111 - 13
patina/src/lib.rs

@@ -1,49 +1,147 @@
-use platform::EventInterface;
+use std::ops::{Deref, DerefMut};
 
+use layout::LayoutNodeAccess;
+use platform::{
+    render::{Painter, Surface},
+    EventInterface, RenderInterface,
+};
+
+pub mod component;
 pub mod geom;
+pub mod input;
 pub mod layout;
 pub mod platform;
-pub mod component;
 pub mod widget;
-pub mod input;
 
 pub mod prelude {
+    pub use crate::component::Component;
+    pub use crate::widget::Widget;
+}
+use prelude::*;
+
+pub enum UIControlMsg {
+    Terminate,
+}
+
+pub trait UIComponent: Sized + component::Component<ParentMsg = UIControlMsg> {
+    fn build(bc: &widget::BuildContext) -> Self;
 
+    fn map_ui_event(&self, uievent: platform::event::UIEvent) -> Option<Self::Msg>;
+
+    type RootWidget: widget::Widget<Self>;
+    fn root_widget(&self) -> &Self::RootWidget;
+    fn root_widget_mut(&mut self) -> &mut Self::RootWidget;
 }
 
-pub struct UI<PT: platform::PlatformTuple> {
+pub struct UI<PT: platform::PlatformTuple, UIC: UIComponent> {
     layout_cache: std::rc::Rc<layout::LayoutCache>,
+    root_layout_node: layout::LayoutNode,
     platform: PT,
+    ui_component: UIC,
+    input_state: input::InputState,
+    ui_running: bool,
 }
 
-impl<PT: platform::PlatformTuple> UI<PT> {
+impl<PT: platform::PlatformTuple, UIC: UIComponent> UI<PT, UIC> {
     pub fn new(pt: PT) -> Self {
+        let lcache = layout::LayoutCache::new();
+        let bc = widget::BuildContext {
+            lcache: lcache.clone(),
+        };
         Self {
-            layout_cache: layout::LayoutCache::new(),
+            root_layout_node: layout::LayoutNode::new(lcache.clone()),
+            layout_cache: lcache,
             platform: pt,
+            ui_component: UIC::build(&bc),
+            input_state: Default::default(),
+            ui_running: true,
+        }
+    }
+
+    pub fn is_ui_running(&self) -> bool {
+        self.ui_running
+    }
+
+    pub fn ui_component(&self) -> &UIC {
+        &self.ui_component
+    }
+    pub fn ui_component_mut(&mut self) -> &mut UIC {
+        &mut self.ui_component
+    }
+
+    /// Public helper function, delivers a message to the UI component and then processes whatever
+    /// control messages result.
+    pub fn deliver_msg(&mut self, msg: UIC::Msg) {
+        let cmsgs = self.ui_component.process(msg);
+        self.process_control_msgs(cmsgs.into_iter());
+    }
+
+    fn process_control_msgs(&mut self, msgs: impl Iterator<Item = UIControlMsg>) {
+        for cmsg in msgs {
+            match cmsg {
+                UIControlMsg::Terminate => {
+                    self.ui_running = false;
+                }
+            }
         }
     }
 
     pub fn update(&mut self) {
+        let mut platform_events = vec![];
         while let Some(evt) = self.platform.event_mut().poll_event() {
             match evt {
                 platform::event::WindowEvent::CloseRequest => {
-                    std::process::exit(0);
+                    platform_events.extend(
+                        self.ui_component()
+                            .map_ui_event(platform::event::UIEvent::CloseRequest)
+                            .into_iter(),
+                    );
                 }
-                _ => {},
+                _ => {}
             }
         }
+
+        // poll for state changes due to user input
+        let poll_msgs = self
+            .ui_component
+            .root_widget_mut()
+            .poll(Some(&self.input_state))
+            .into_iter();
+        let cmsgs = self
+            .ui_component
+            .process_all(platform_events.into_iter().chain(poll_msgs));
+        self.process_control_msgs(cmsgs.into_iter());
+
+        let tgt_size = self.platform.render_mut().render_target_mut().size();
+        self.root_layout_node
+            .set_behaviour(layout::NodeBehaviour::Fixed {
+                rect: geom::IRect::new_from_size(geom::IPoint::ORIGIN, tgt_size),
+            });
+
+        // first, make sure we can paint to the render target
+        let (ri, ti) = self.platform.render_text_mut();
+        if let Some(mut painter) = ri.render_target_mut().painter() {
+            // perform layout as needed
+            layout::recalculate(LayoutNodeAccess::new(&layout::LinearAccess::new(
+                &self.root_layout_node,
+                self.ui_component.root_widget().layout_node(),
+            )));
+
+            // now render as needed
+            self.ui_component.root_widget().render(painter.as_mut(), ti);
+        } else {
+            log::warn!("Could not draw UI: no painter available");
+        }
     }
 
-    pub fn sink_event(&mut self, evt: platform::event::UIEvent) {
-        
+    pub fn wait(&mut self) {
+        self.platform.event_mut().wait_for_event();
     }
 
     pub fn run(mut self) {
-        loop {
+        while self.ui_running {
             self.update();
-
-            self.platform.event_mut().wait_event();
+            self.wait();
         }
     }
 }

+ 15 - 3
patina/src/platform.rs

@@ -1,15 +1,20 @@
 pub mod render;
 
-pub trait RenderInterface {}
+pub trait RenderInterface {
+    type RenderTarget: render::Surface;
+    fn render_target_mut(&mut self) -> &mut Self::RenderTarget;
+}
 
 pub mod event;
 
 pub trait EventInterface {
     fn poll_event(&mut self) -> Option<event::WindowEvent>;
-    fn wait_event(&mut self);
+    fn wait_for_event(&mut self);
 }
 
-pub trait TextInterface {}
+pub trait TextInterface {
+    fn render_text(&self, what: &str, sz: f32) -> Box<dyn render::Surface + '_>;
+}
 
 pub trait PlatformTuple {
     type Render: RenderInterface;
@@ -24,6 +29,9 @@ pub trait PlatformTuple {
 
     fn text(&self) -> &Self::Text;
     fn text_mut(&mut self) -> &mut Self::Text;
+
+    // subset access
+    fn render_text_mut(&mut self) -> (&mut Self::Render, &mut Self::Text);
 }
 
 impl<RI: RenderInterface, EI: EventInterface, TI: TextInterface> PlatformTuple for (RI, EI, TI) {
@@ -51,4 +59,8 @@ impl<RI: RenderInterface, EI: EventInterface, TI: TextInterface> PlatformTuple f
     fn text_mut(&mut self) -> &mut Self::Text {
         &mut self.2
     }
+
+    fn render_text_mut(&mut self) -> (&mut Self::Render, &mut Self::Text) {
+        (&mut self.0, &mut self.2)
+    }
 }

+ 7 - 26
patina/src/platform/event.rs

@@ -1,35 +1,16 @@
-use crate::geom::IPoint;
-
-#[derive(Clone, Copy, Debug)]
-pub struct ButtonSet(u8);
-
-pub struct MouseState {
-    pub pos: IPoint,
-    pub buttons: ButtonSet,
-    pub last_buttons: ButtonSet,
-}
-
-impl MouseState {
-    pub fn pressed(&self) -> ButtonSet {
-        ButtonSet(self.buttons.0 & !self.last_buttons.0)
-    }
-
-    pub fn released(&self) -> ButtonSet {
-        ButtonSet(self.last_buttons.0 & !self.buttons.0)
-    }
-}
+// use crate::geom::IPoint;
 
 #[derive(Clone, Debug)]
 pub enum UIEvent {
-    MouseUpdate,
-    KeyDown,
-    KeyUp,
-    TextInput,
+    CloseRequest,
+    Resize,
 }
 
 #[derive(Clone, Debug)]
 pub enum WindowEvent {
-    RepaintRequest,
-    Resize,
     CloseRequest,
+    MouseUpdate,
+    KeyDown,
+    KeyUp,
+    TextInput,
 }

+ 57 - 46
patina/src/platform/render.rs

@@ -1,66 +1,77 @@
-#[derive(Clone, Copy, Debug)]
-pub enum SurfaceFormat {
-    RGBA,
-    ARGB,
-    ABGR,
-    RGB,
-    GBR,
+use crate::geom::{IPoint, IRect, IVector};
+
+/// Simple colour representation
+pub struct Colour {
+    comp: [u8; 4],
 }
 
-impl SurfaceFormat {
-    pub fn stride(&self) -> usize {
-        match self {
-            Self::RGBA | Self::ARGB | Self::ABGR => 4,
-            Self::RGB | Self::GBR => 3,
-        }
-    }
+/// Type alias for Americans
+pub type Color = Colour;
 
-    /// Red colour value bitmask, expressed as (bit-index, len)
-    pub const fn red_mask(&self) -> (u8, u8) {
-        match self {
-            Self::RGBA => (0, 8),
-            Self::RGB => (0, 8),
-            _ => todo!(),
+impl Colour {
+    pub fn new_rgb(r: u8, g: u8, b: u8) -> Self {
+        Self {
+            comp: [r, g, b, 255u8],
         }
     }
-
-    /// Green colour value bitmask, expressed as (bit-index, len)
-    pub const fn green_mask(&self) -> (u8, u8) {
-        match self {
-            Self::RGBA => (8, 8),
-            Self::RGB => (8, 8),
-            _ => todo!(),
-        }
+    pub fn new_rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
+        Self { comp: [r, g, b, a] }
     }
 
-    /// Blue colour value bitmask, expressed as (bit-index, len)
-    pub const fn blue_mask(&self) -> (u8, u8) {
-        match self {
-            Self::RGBA => (16, 8),
-            Self::RGB => (16, 8),
-            _ => todo!(),
-        }
+    /// Red component of colour
+    pub fn r(&self) -> u8 {
+        self.comp[0]
     }
-
-    pub const fn alpha_mask(&self) -> (u8, u8) {
-        match self {
-            Self::RGBA => (24, 8),
-            Self::RGB => (0, 0),
-            _ => todo!(),
-        }
+    /// Green component of colour
+    pub fn g(&self) -> u8 {
+        self.comp[1]
+    }
+    /// Blue component of colour
+    pub fn b(&self) -> u8 {
+        self.comp[2]
+    }
+    /// Alpha component of colour (0=transparent,255=opaque)
+    pub fn a(&self) -> u8 {
+        self.comp[3]
     }
 }
 
+pub struct ColourMask {
+    pub base: u8,
+    pub len: u8,
+}
+
 #[derive(Clone, Copy, Debug)]
 pub enum AccessOrder {
     RowMajor,
     ColumnMajor,
 }
 
+pub trait SurfaceFormat {
+    const PIXEL_STRIDE: usize;
+    const ACCESS: AccessOrder;
+    const RED_MASK: ColourMask;
+    const GREEN_MASK: ColourMask;
+    const BLUE_MASK: ColourMask;
+    const ALPHA_MASK: ColourMask;
+}
+
 pub trait Surface {
-    fn width(&self) -> usize;
-    fn height(&self) -> usize;
+    // type Format: SurfaceFormat;
+
+    fn size(&self) -> IVector;
 
-    fn access(&self) -> AccessOrder;
-    fn format(&self) -> SurfaceFormat;
+    fn painter(&mut self) -> Option<Box<dyn Painter + '_>>;
 }
+
+pub trait Painter {
+    fn blit(&mut self, source: &dyn Surface, source_area: IRect, target: IPoint);
+    fn fill(&mut self, area: IRect, col: Colour);
+    fn outline_rect(&mut self, rect: IRect, col: Colour);
+}
+
+/*pub struct DefaultPainter<TS: Surface> {
+    surf: TS,
+}
+
+impl<TS: Surface> */

+ 23 - 8
patina/src/widget.rs

@@ -1,21 +1,36 @@
-use std::rc::{Rc, Weak};
+use crate::{
+    component::Component,
+    input::InputState,
+    layout::{LayoutCache, LayoutNode, LayoutNodeAccess},
+    platform::{render::Painter, TextInterface},
+};
 
-use crate::{component::Component, input::InputState, layout::LayoutNodeAccess};
+mod frame;
+mod group;
+mod button;
+mod label;
+
+pub use frame::Frame;
+pub use group::PlainGroup;
+pub use label::Label;
 
 #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
 pub struct WidgetID(usize);
 static NEXT_WIDGET_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
 impl WidgetID {
     pub fn new() -> Self {
-        Self(
-            NEXT_WIDGET_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
-        )
+        Self(NEXT_WIDGET_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
     }
 }
 
 pub trait Widget<C: Component>: 'static {
-    fn poll(&mut self, input_state: &InputState) -> impl Iterator<Item = C::Msg>;
+    fn poll(&mut self, input_state: Option<&InputState>) -> Vec<C::Msg>;
+
+    fn layout_node(&self) -> LayoutNodeAccess;
+    fn layout_node_mut(&mut self) -> &mut LayoutNode;
+    fn render(&self, painter: &mut dyn Painter, ti: &dyn TextInterface);
+}
 
-    fn layout_node(&self) -> &dyn LayoutNodeAccess;
-    fn render(&self, surface: ());
+pub struct BuildContext {
+    pub(crate) lcache: std::rc::Rc<LayoutCache>,
 }

+ 0 - 0
patina/src/widget/button.rs


+ 59 - 0
patina/src/widget/frame.rs

@@ -0,0 +1,59 @@
+use crate::{
+    component::Component,
+    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer},
+    platform::{render::{Colour, Painter}, TextInterface},
+};
+
+use super::{BuildContext, Widget};
+
+pub struct Frame<C: Component> {
+    layout: LayoutNode,
+    child: Option<Box<dyn Widget<C>>>,
+    _ghost: std::marker::PhantomData<C>,
+}
+
+impl<C: Component> Frame<C> {
+    pub fn new(bc: &BuildContext) -> Self {
+        Self {
+            layout: LayoutNode::new(bc.lcache.clone()),
+            child: None,
+            _ghost: Default::default(),
+        }
+    }
+
+    pub fn set_child(&mut self, child: Box<dyn Widget<C>>) {
+        self.child = Some(child);
+        self.layout.relayout();
+    }
+}
+
+impl<C: Component> LayoutNodeContainer for Frame<C> {
+    fn layout_node(&self) -> &LayoutNode {
+        &self.layout
+    }
+    fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess> {
+        if ndx == 0 { Some(self.child.as_ref()?.layout_node()) } else { None }
+    }
+    fn layout_child_count(&self) -> usize {
+        if self.child.is_some() { 1 } else { 0 }
+    }
+}
+
+impl<C: Component> Widget<C> for Frame<C> {
+    fn poll(&mut self, _input_state: Option<&crate::input::InputState>) -> Vec<C::Msg> {
+        vec![]
+    }
+    fn layout_node(&self) -> LayoutNodeAccess {
+        LayoutNodeAccess::new(self)
+    }
+    fn layout_node_mut(&mut self) -> &mut LayoutNode {
+        &mut self.layout
+    }
+    fn render(&self, painter: &mut dyn Painter, ti: &dyn TextInterface) {
+        let rect = self.layout.cached_rect().unwrap();
+        painter.fill(rect, Colour::new_rgb(50, 42, 0));
+        if let Some(child) = self.child.as_ref() {
+            child.render(painter, ti);
+        }
+    }
+}

+ 49 - 0
patina/src/widget/group.rs

@@ -0,0 +1,49 @@
+use crate::{
+    layout::{LayoutChildIter, LayoutNode, LayoutNodeAccess, LayoutNodeContainer}, platform::{render::Painter, TextInterface}, Component, Widget
+};
+
+pub struct PlainGroup<C: Component> {
+    lnode: LayoutNode,
+    children: Vec<Box<dyn Widget<C>>>,
+}
+
+impl<C: Component> std::ops::Deref for PlainGroup<C> {
+    type Target = LayoutNode;
+    fn deref(&self) -> &Self::Target {
+        &self.lnode
+    }
+}
+
+impl<C: Component> LayoutNodeContainer for PlainGroup<C> {
+    fn layout_node(&self) -> &LayoutNode {
+        &self.lnode
+    }
+
+    fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess> {
+        self.children.get(ndx).map(|w| w.layout_node())
+    }
+
+    fn layout_child_count(&self) -> usize {
+        self.children.len()
+    }
+}
+
+impl<C: Component> Widget<C> for PlainGroup<C> {
+    fn layout_node(&self) -> LayoutNodeAccess {
+        LayoutNodeAccess::new(self)
+    }
+    fn layout_node_mut(&mut self) -> &mut LayoutNode {
+        &mut self.lnode
+    }
+    fn poll(
+        &mut self,
+        input_state: Option<&crate::input::InputState>,
+    ) -> Vec<<C as Component>::Msg> {
+        vec![]
+    }
+    fn render(&self, painter: &mut dyn Painter, ti: &dyn TextInterface) {
+        for child in self.children.iter() {
+            child.render(painter, ti);
+        }
+    }
+}

+ 34 - 0
patina/src/widget/label.rs

@@ -0,0 +1,34 @@
+use crate::{layout::{LayoutNode, LayoutNodeAccess, LeafLayoutNode}, platform::{render::{Colour, Painter}, TextInterface}, Component, Widget};
+
+use super::BuildContext;
+
+pub struct Label<C: Component> {
+    lnode: LeafLayoutNode,
+    _ghost: std::marker::PhantomData<C>,
+}
+
+impl<C: Component> Label<C> {
+    pub fn new(bc: &BuildContext) -> Self {
+        Self {
+            lnode: LayoutNode::new(bc.lcache.clone()).into(),
+            _ghost: Default::default(),
+        }
+    }
+}
+
+impl<C: Component> Widget<C> for Label<C> {
+    fn poll(&mut self, input_state: Option<&crate::input::InputState>) -> Vec<<C as Component>::Msg> {
+        vec![]
+    }
+
+    fn layout_node(&self) -> LayoutNodeAccess {
+        LayoutNodeAccess::new(&self.lnode)
+    }
+    fn layout_node_mut(&mut self) -> &mut LayoutNode {
+        &mut self.lnode
+    }
+
+    fn render(&self, painter: &mut dyn Painter, _ti: &dyn TextInterface) {
+        painter.fill(self.layout_node().cached_rect().unwrap(), Colour::new_rgb(64, 64, 64));
+    }
+}