Browse Source

Simple button example that changes colour when hovered and exits on click.

Kestrel 5 months ago
parent
commit
b88087436f
16 changed files with 495 additions and 141 deletions
  1. 66 0
      examples/button.rs
  2. 12 2
      src/input.rs
  3. 8 0
      src/input/button.rs
  4. 63 37
      src/layout.rs
  5. 12 12
      src/layout/arr.rs
  6. 1 1
      src/layout/cache.rs
  7. 15 9
      src/layout/calc.rs
  8. 5 0
      src/lib.rs
  9. 42 32
      src/text.rs
  10. 18 0
      src/ui.rs
  11. 6 2
      src/widget.rs
  12. 109 0
      src/widget/button.rs
  13. 18 7
      src/widget/frame.rs
  14. 31 14
      src/widget/group.rs
  15. 36 12
      src/widget/label.rs
  16. 53 13
      src/window.rs

+ 66 - 0
examples/button.rs

@@ -0,0 +1,66 @@
+use patina::{
+    layout::SizePolicy,
+    prelude::*,
+    ui::{UIControlMsg, UIHandle},
+    widget,
+};
+
+enum ButtonWindowMsg {
+    Shutdown,
+}
+
+struct ButtonWindow {
+    group: widget::PlainGroup<Self>,
+}
+
+impl ButtonWindow {
+    fn new(uih: &mut UIHandle) -> Self {
+        let mut group = widget::PlainGroup::new(uih);
+        group
+            .layout_node_mut()
+            .set_arrangement(patina::layout::ChildArrangement::Column)
+            .set_width_policy(SizePolicy::expands(1))
+            .set_height_policy(SizePolicy::expands(1));
+        group.append(Box::new(widget::Spacer::new(uih)));
+        group.append(Box::new({
+            let mut button = widget::Button::new(uih);
+            button.set_label("Button label");
+            button.set_hook(Box::new(|| Some(ButtonWindowMsg::Shutdown)));
+            button
+        }));
+        group.append(Box::new(widget::Spacer::new(uih)));
+        Self { group }
+    }
+}
+
+impl Component for ButtonWindow {
+    type ParentMsg = UIControlMsg;
+    type Msg = ButtonWindowMsg;
+
+    fn process(&mut self, msg: Self::Msg) -> Vec<Self::ParentMsg> {
+        match msg {
+            ButtonWindowMsg::Shutdown => vec![UIControlMsg::Terminate],
+        }
+    }
+}
+
+impl WindowComponent for ButtonWindow {
+    fn map_window_event(&self, we: patina::window::WindowEvent) -> Option<Self::Msg> {
+        match we {
+            patina::window::WindowEvent::CloseRequested => Some(ButtonWindowMsg::Shutdown),
+            _ => None,
+        }
+    }
+
+    type RootWidget = widget::PlainGroup<Self>;
+    fn root_widget(&self) -> &Self::RootWidget {
+        &self.group
+    }
+    fn root_widget_mut(&mut self) -> &mut Self::RootWidget {
+        &mut self.group
+    }
+}
+
+fn main() {
+    patina::ui::make_opinionated_ui(ButtonWindow::new).run();
+}

+ 12 - 2
src/input.rs

@@ -21,6 +21,8 @@ impl Into<u8> for MouseButton {
     }
 }
 
+pub type MouseButtonSet = ButtonSet<MouseButton>;
+
 #[derive(Default)]
 pub struct MouseState {
     pub pos: kahlo::math::PixelPoint,
@@ -30,10 +32,10 @@ pub struct MouseState {
 }
 
 impl MouseState {
-    pub fn pressed(&self) -> ButtonSet<MouseButton> {
+    pub fn pressed(&self) -> MouseButtonSet {
         self.buttons & !self.last_buttons
     }
-    pub fn released(&self) -> ButtonSet<MouseButton> {
+    pub fn released(&self) -> MouseButtonSet {
         !self.buttons & self.last_buttons
     }
 }
@@ -42,3 +44,11 @@ impl MouseState {
 pub struct InputState {
     pub mouse: MouseState,
 }
+
+impl InputState {
+    /// Note the passing of a frame.
+    pub fn tick(&mut self) {
+        self.mouse.last_pos = self.mouse.pos;
+        self.mouse.last_buttons = self.mouse.buttons;
+    }
+}

+ 8 - 0
src/input/button.rs

@@ -11,6 +11,14 @@ impl<BS: ButtonSpec> ButtonSet<BS> {
     pub fn inactive(&self, button: BS) -> bool {
         !self.active(button)
     }
+
+    pub fn set_button(&mut self, button: BS, to: bool) {
+        if to {
+            self.0 |= 1 << button.into();
+        } else {
+            self.0 &= !(1 << button.into());
+        }
+    }
 }
 
 impl<BS: ButtonSpec> Default for ButtonSet<BS> {

+ 63 - 37
src/layout.rs

@@ -1,7 +1,7 @@
 use std::{ops::Deref, rc::Rc};
 
 use cache::{Layer, LayoutCacheKey, NodeState};
-use kahlo::math::{PixelBox, PixelRect};
+use kahlo::math::{PixelBox, PixelSideOffsets};
 
 mod arr;
 mod cache;
@@ -10,8 +10,9 @@ mod calc;
 pub use cache::LayoutCache;
 pub use calc::recalculate;
 
-/// Sizing policy for a layout dimension
-#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
+/// 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)]
 pub struct SizePolicy {
     /// The minimum number of pixels required along this dimension to avoid overlap and other
     /// issues.
@@ -23,6 +24,16 @@ pub struct SizePolicy {
     pub slack_weight: usize,
 }
 
+impl Default for SizePolicy {
+    fn default() -> Self {
+        Self {
+            minimum: 0,
+            desired: 0,
+            slack_weight: 1,
+        }
+    }
+}
+
 impl SizePolicy {
     pub fn expands(weight: usize) -> Self {
         Self {
@@ -60,28 +71,30 @@ impl std::ops::Add<Self> for SizePolicy {
     }
 }
 
-#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
-pub struct BoxMeasure {
-    pub left: usize,
-    pub right: usize,
-    pub top: usize,
-    pub bottom: usize,
+/// How to horizontally align a smaller node inside a larger node.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum HorizontalAlignment {
+    Left,
+    Centre,
+    Right,
 }
 
-impl From<usize> for BoxMeasure {
-    fn from(value: usize) -> Self {
-        Self::new_from_value(value)
+impl Default for HorizontalAlignment {
+    fn default() -> Self {
+        Self::Centre
     }
 }
 
-impl BoxMeasure {
-    pub fn new_from_value(value: usize) -> Self {
-        Self {
-            left: value,
-            right: value,
-            top: value,
-            bottom: value,
-        }
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum VerticalAlignment {
+    Top,
+    Centre,
+    Bottom,
+}
+
+impl Default for VerticalAlignment {
+    fn default() -> Self {
+        Self::Centre
     }
 }
 
@@ -89,7 +102,7 @@ impl BoxMeasure {
 #[derive(Clone, Copy, PartialEq, Debug)]
 pub enum NodeBehaviour {
     /// A fixed rendering area, probably a root node.
-    Fixed { rect: PixelRect },
+    Fixed { area: PixelBox },
     /// An ordinary box sitting inside another node, hinting its size and receiving a final
     /// size assignment from its parent.
     Element,
@@ -136,9 +149,10 @@ pub struct LayoutNode {
     child_arrangement: ChildArrangement,
     width_policy: SizePolicy,
     height_policy: SizePolicy,
+    halign: HorizontalAlignment,
+    valign: VerticalAlignment,
 
-    padding: BoxMeasure,
-    margin: BoxMeasure,
+    margin: PixelSideOffsets,
 }
 
 impl std::fmt::Debug for LayoutNode {
@@ -150,7 +164,6 @@ impl std::fmt::Debug for LayoutNode {
             .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()
     }
@@ -168,8 +181,9 @@ impl LayoutNode {
             child_arrangement: ChildArrangement::Column,
             width_policy: SizePolicy::default(),
             height_policy: SizePolicy::default(),
-            padding: 0.into(),
-            margin: 0.into(),
+            halign: HorizontalAlignment::default(),
+            valign: VerticalAlignment::default(),
+            margin: PixelSideOffsets::new_all_same(0),
         }
     }
 
@@ -177,14 +191,15 @@ impl LayoutNode {
         self.cache.update_queue.borrow_mut().push(self.cache_key);
     }
 
-    pub fn cached_rect(&self) -> Option<PixelRect> {
+    pub fn render_area(&self) -> Option<PixelBox> {
         self.cache
-            .with_state(self.cache_key, |ns| ns.rect)
+            .with_state(self.cache_key, |ns| ns.area)
             .flatten()
+            .map(|pb| pb.inner_box(self.margin))
     }
 }
 
-// accessors
+/// Accessors
 impl LayoutNode {
     pub fn label(&self) -> Option<&str> {
         self.label.as_ref().map(String::as_str)
@@ -228,16 +243,25 @@ impl LayoutNode {
         self
     }
 
-    pub fn padding(&self) -> BoxMeasure {
-        self.padding
+    pub fn halign(&self) -> HorizontalAlignment {
+        self.halign
     }
-    pub fn padding_mut(&mut self) -> &mut BoxMeasure {
-        &mut self.padding
+    pub fn set_halign(&mut self, halign: HorizontalAlignment) -> &mut Self {
+        self.halign = halign;
+        self
     }
-    pub fn margin(&self) -> BoxMeasure {
+    pub fn valign(&self) -> VerticalAlignment {
+        self.valign
+    }
+    pub fn set_valign(&mut self, valign: VerticalAlignment) -> &mut Self {
+        self.valign = valign;
+        self
+    }
+
+    pub fn margin(&self) -> PixelSideOffsets {
         self.margin
     }
-    pub fn margin_mut(&mut self) -> &mut BoxMeasure {
+    pub fn margin_mut(&mut self) -> &mut PixelSideOffsets {
         &mut self.margin
     }
 }
@@ -252,7 +276,8 @@ impl Clone for LayoutNode {
             child_arrangement: self.child_arrangement.clone(),
             width_policy: self.width_policy,
             height_policy: self.height_policy,
-            padding: self.padding,
+            halign: self.halign,
+            valign: self.valign,
             margin: self.margin,
         }
     }
@@ -287,7 +312,7 @@ fn dump_node_tree_helper(lna: LayoutNodeAccess, indent: usize, out: &mut String)
     );
     out.push_str(ind.as_str());
     out.push_str("        ");
-    out.push_str(format!("cached rect: {:?}\n", lna.cached_rect()).as_str());
+    out.push_str(format!("render area: {:?}\n", lna.render_area()).as_str());
 
     for child in lna.child_iter() {
         dump_node_tree_helper(child, indent + 1, out);
@@ -326,6 +351,7 @@ impl<'l> Iterator for LayoutChildIter<'l> {
     }
 }
 
+/// Wrapper struct to access a [`LayoutNodeContainer`].
 #[derive(Clone, Copy)]
 pub struct LayoutNodeAccess<'l> {
     lnc: &'l dyn LayoutNodeContainer,

+ 12 - 12
src/layout/arr.rs

@@ -1,4 +1,4 @@
-use kahlo::math::{PixelPoint, PixelRect, PixelSize};
+use kahlo::math::{PixelBox, PixelPoint, PixelSize};
 use std::ops::Add;
 
 use super::{cache::NodeState, LayoutNodeAccess, SizePolicy};
@@ -9,7 +9,7 @@ pub trait ArrangementCalculator {
         node: LayoutNodeAccess,
         child_policies: Vec<(SizePolicy, SizePolicy)>,
     ) -> (SizePolicy, SizePolicy);
-    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelRect);
+    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox);
 }
 
 #[derive(Clone, Debug)]
@@ -61,10 +61,10 @@ impl ArrangementCalculator for LineArrangement {
         )
     }
 
-    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelRect) {
+    fn layout_step(&self, node: LayoutNodeAccess, inside: PixelBox) {
         // do the final children layouts
         node.cache
-            .with_state(node.cache_key, |ns| ns.rect = Some(inside));
+            .with_state(node.cache_key, |ns| ns.area = Some(inside));
 
         if node.child_len() == 0 {
             return;
@@ -90,11 +90,11 @@ impl ArrangementCalculator for LineArrangement {
             policies,
         );
         for (offset, child) in fit.zip(node.child_iter()) {
-            let crect = if self.is_column() {
-                PixelRect::new(
+            let cbox = if self.is_column() {
+                PixelBox::from_origin_and_size(
                     PixelPoint {
-                        x: inside.min_x(),
-                        y: inside.min_y() + last_offset,
+                        x: inside.min.x,
+                        y: inside.min.y + last_offset,
                         ..Default::default()
                     },
                     PixelSize {
@@ -104,10 +104,10 @@ impl ArrangementCalculator for LineArrangement {
                     },
                 )
             } else {
-                PixelRect::new(
+                PixelBox::from_origin_and_size(
                     PixelPoint {
-                        x: inside.min_x() + last_offset,
-                        y: inside.min_y(),
+                        x: inside.min.x + last_offset,
+                        y: inside.min.y,
                         ..Default::default()
                     },
                     PixelSize {
@@ -118,7 +118,7 @@ impl ArrangementCalculator for LineArrangement {
                 )
             };
 
-            self.layout_step(child, crect);
+            self.layout_step(child, cbox);
             last_offset = offset;
         }
     }

+ 1 - 1
src/layout/cache.rs

@@ -16,7 +16,7 @@ pub struct NodeState {
     pub(super) needs_update: bool,
     pub(super) net_policy: (SizePolicy, SizePolicy),
     pub(super) layer: Layer,
-    pub(super) rect: Option<kahlo::math::PixelRect>,
+    pub(super) area: Option<kahlo::math::PixelBox>,
     pub(super) children: Vec<LayoutCacheKey>,
 }
 

+ 15 - 9
src/layout/calc.rs

@@ -1,7 +1,7 @@
-use super::{cache::LayoutCacheKey, Layer, LayoutNode, LayoutNodeAccess, NodeBehaviour, NodeState};
+use super::{cache::LayoutCacheKey, Layer, LayoutNodeAccess, NodeBehaviour, NodeState};
 
 pub fn recalculate(node: LayoutNodeAccess) {
-    if let NodeBehaviour::Fixed { rect } = node.behaviour() {
+    if let NodeBehaviour::Fixed { area } = node.behaviour() {
         let layer = Layer::default();
 
         // propagate relayout flags up to the root
@@ -9,7 +9,7 @@ pub fn recalculate(node: LayoutNodeAccess) {
 
         arrangement_pass(None, node, layer);
 
-        node.child_arrangement.layout_step(node, rect);
+        node.child_arrangement.layout_step(node, area);
     } else {
         log::warn!("Layout root node does not have Fixed mode");
     }
@@ -48,7 +48,7 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, laye
             NodeState {
                 needs_update: false,
                 net_policy: (wpol, hpol),
-                rect: None,
+                area: None,
                 layer,
                 children: vec![],
             },
@@ -58,7 +58,7 @@ fn arrangement_pass(parent: Option<LayoutCacheKey>, node: LayoutNodeAccess, laye
 
 #[cfg(test)]
 mod test {
-    use kahlo::math::{PixelPoint, PixelRect, PixelSize};
+    use kahlo::math::{PixelBox, PixelPoint, PixelRect, PixelSize};
 
     use crate::layout::{
         cache::LayoutCache, ChildArrangement, LayoutNode, LayoutNodeAccess, LayoutNodeContainer,
@@ -129,16 +129,22 @@ mod test {
         // check that final rects match expectations
         assert_eq!(
             cache
-                .with_state(root.node.cache_key, |ns| ns.rect)
+                .with_state(root.node.cache_key, |ns| ns.area)
                 .flatten(),
-            Some(PixelRect::new(PixelPoint::new(1, 1), PixelSize::new(2, 5)))
+            Some(PixelBox::from_origin_and_size(
+                PixelPoint::new(1, 1),
+                PixelSize::new(2, 5)
+            ))
         );
 
         assert_eq!(
             cache
-                .with_state(root.children[0].node.cache_key, |ns| ns.rect)
+                .with_state(root.children[0].node.cache_key, |ns| ns.area)
                 .flatten(),
-            Some(PixelRect::new(PixelPoint::new(1, 1), PixelSize::new(2, 2)))
+            Some(PixelBox::from_origin_and_size(
+                PixelPoint::new(1, 1),
+                PixelSize::new(2, 2)
+            ))
         );
     }
 }

+ 5 - 0
src/lib.rs

@@ -10,9 +10,14 @@ pub mod window;
 pub mod prelude {
     pub use crate::component::Component;
     pub use crate::widget::Widget;
+    pub use crate::window::WindowComponent;
 }
 
 pub mod re_exports {
     pub use fontdue;
     pub use kahlo;
 }
+
+pub mod math {
+    pub use kahlo::math::*;
+}

+ 42 - 32
src/text.rs

@@ -1,5 +1,5 @@
 use kahlo::{
-    math::{PixelBox, PixelPoint},
+    math::{PixelBox, PixelPoint, PixelSize},
     prelude::*,
 };
 
@@ -14,54 +14,64 @@ impl<'l> TextLine<'l> {
         Self { line, font, size }
     }
 
-    pub fn render_line(&self) -> kahlo::Alphamap {
-        // pass 1: determine required size
-        let baseline_offsets = || {
-            let mut running_offset = 0;
-            let mut last_char = None;
-
-            self.line.chars().map(move |ch| {
-                let metrics = self.font.metrics(ch, self.size);
-
-                let glyph_width = if let Some(last) = last_char.take() {
-                    metrics.advance_width
-                        + self.font.horizontal_kern(last, ch, self.size).unwrap_or(0.)
-                } else {
-                    metrics.advance_width
-                };
-
-                last_char = Some(ch);
-
-                let expose = (running_offset, glyph_width.round() as u32, ch);
-                running_offset += glyph_width.round() as u32;
-                expose
-            })
-        };
+    fn generate_baseline_offsets(&self) -> impl '_ + Iterator<Item = (u32, u32, char)> {
+        let mut running_offset = 0;
+        let mut last_char = None;
+
+        self.line.chars().map(move |ch| {
+            let metrics = self.font.metrics(ch, self.size);
+
+            let glyph_width = if let Some(last) = last_char.take() {
+                metrics.advance_width + self.font.horizontal_kern(last, ch, self.size).unwrap_or(0.)
+            } else {
+                metrics.advance_width
+            };
+
+            last_char = Some(ch);
+
+            let expose = (running_offset, glyph_width.round() as u32, ch);
+            running_offset += glyph_width.round() as u32;
+            expose
+        })
+    }
 
+    pub fn calculate_size(&self) -> PixelSize {
+        // pass: determine required size
         let total_width = {
-            let ofs = baseline_offsets().last().unwrap();
+            let ofs = self.generate_baseline_offsets().last().unwrap();
             ofs.0 + ofs.1
         };
+        let line_metrics = self.font.horizontal_line_metrics(self.size).unwrap();
+        let line_height = line_metrics.new_line_size.ceil() as i32;
+
+        PixelSize::new(total_width as i32, line_height)
+    }
+
+    pub fn render_line(&self) -> kahlo::Alphamap {
+        // pass 1: determine required size
+        let alphamap_size = self.calculate_size();
 
         let line_metrics = self.font.horizontal_line_metrics(self.size).unwrap();
-        let line_height = line_metrics.new_line_size.ceil() as usize;
         let baseline = line_metrics.ascent.ceil() as usize;
-        let mut alphamap = kahlo::Alphamap::new(total_width as usize, line_height * 2);
+        let mut alphamap =
+            kahlo::Alphamap::new(alphamap_size.width as usize, alphamap_size.height as usize);
 
-        // pass 2: generate alphamap
-        for (offset, _width, ch) in baseline_offsets() {
+        // pass: generate alphamap
+        for (offset, _width, ch) in self.generate_baseline_offsets() {
             let (metrics, raster) = self.font.rasterize(ch, self.size);
             let character_alphamap = kahlo::BitmapRef::<kahlo::formats::A8>::new(
                 raster.as_slice(),
                 metrics.width,
                 metrics.height,
-                metrics.width,
             );
 
             alphamap.copy_from(
                 &character_alphamap,
-                &PixelBox::from_size(character_alphamap.size()),
-                &PixelPoint::new(metrics.xmin + offset as i32, baseline as i32 - (metrics.height as i32 + metrics.ymin as i32)),
+                PixelBox::from_size(character_alphamap.size()),
+                PixelPoint::new(
+                    metrics.xmin + offset as i32,
+                    baseline as i32 - (metrics.height as i32 + metrics.ymin as i32),
+                ),
             );
         }
 

+ 18 - 0
src/ui.rs

@@ -1,7 +1,10 @@
 use std::collections::HashMap;
 
+use kahlo::math::PixelPoint;
+
 use crate::{
     component::Component,
+    input::MouseButton,
     layout::{LayoutCache, LayoutNode},
     theme::Theme,
     window::{Window, WindowBuilder, WindowComponent, WindowEvent, WindowStateAccess},
@@ -170,6 +173,21 @@ impl<UIC: UIComponent> winit::application::ApplicationHandler<()> for UI<UIC> {
                 wsa.request_redraw();
                 self.pump_events(event_loop);
             }
+            winit::event::WindowEvent::CursorMoved { position, .. } => {
+                wsa.update_mouse_pos(PixelPoint::new(position.x as i32, position.y as i32));
+                wsa.request_redraw();
+                self.pump_events(event_loop);
+            }
+            winit::event::WindowEvent::MouseInput { state, button, .. } => {
+                let button = match button {
+                    winit::event::MouseButton::Left => MouseButton::Left,
+                    winit::event::MouseButton::Right => MouseButton::Right,
+                    _ => return,
+                };
+                wsa.update_mouse_button(button, state.is_pressed());
+                wsa.request_redraw();
+                self.pump_events(event_loop);
+            }
             _ => (),
         }
     }

+ 6 - 2
src/widget.rs

@@ -14,6 +14,10 @@ mod label;
 pub use label::Label;
 mod frame;
 pub use frame::Frame;
+mod group;
+pub use group::PlainGroup;
+mod button;
+pub use button::Button;
 
 pub trait Widget<C: Component> {
     fn poll(&mut self, uih: &mut UIHandle, input_state: Option<&InputState>) -> Vec<C::Msg>;
@@ -58,7 +62,7 @@ impl<C: Component> Widget<C> for Spacer<C> {
     }
 
     fn render(&self, theme: &Theme, target: &mut kahlo::RgbaBitmap) {
-        let region = self.layout_node().cached_rect().unwrap().to_box2d();
-        target.fill_region(&region, theme.background);
+        let region = self.layout_node().render_area().unwrap();
+        target.fill_region(region, theme.background);
     }
 }

+ 109 - 0
src/widget/button.rs

@@ -1 +1,110 @@
+use kahlo::prelude::*;
 
+use crate::{
+    component::Component,
+    input::MouseButton,
+    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy},
+    ui::UIHandle,
+};
+
+use super::{Label, Widget};
+
+enum ButtonState {
+    Idle,
+    Hovered,
+    Clicked,
+}
+
+pub struct Button<C: Component> {
+    layout: LayoutNode,
+    label: Label<C>,
+    state: ButtonState,
+    hook: Option<Box<dyn Fn() -> Option<C::Msg>>>,
+}
+
+impl<C: Component> Button<C> {
+    pub fn new(uih: &UIHandle) -> Self {
+        let mut layout = uih.new_layout_node();
+        layout
+            .set_width_policy(SizePolicy::expands(1))
+            .set_height_policy(SizePolicy {
+                minimum: uih.theme().ui_font_size as usize,
+                desired: (uih.theme().ui_font_size * 1.5) as usize,
+                slack_weight: 0,
+            });
+        Self {
+            layout,
+            label: Label::new(uih),
+            state: ButtonState::Idle,
+            hook: None,
+        }
+    }
+
+    pub fn set_label(&mut self, label: &str) {
+        self.label.set_text(label);
+    }
+
+    pub fn set_hook(&mut self, to: Box<dyn Fn() -> Option<C::Msg>>) {
+        self.hook = Some(to);
+    }
+}
+
+impl<C: Component> LayoutNodeContainer for Button<C> {
+    fn layout_node(&self) -> &LayoutNode {
+        &self.layout
+    }
+    fn layout_child(&self, ndx: usize) -> Option<LayoutNodeAccess<'_>> {
+        if ndx == 0 {
+            Some(self.label.layout_node())
+        } else {
+            None
+        }
+    }
+    fn layout_child_count(&self) -> usize {
+        1
+    }
+}
+
+impl<C: Component> Widget<C> for Button<C> {
+    fn poll(
+        &mut self,
+        uih: &mut UIHandle,
+        input_state: Option<&crate::input::InputState>,
+    ) -> Vec<<C as Component>::Msg> {
+        let mut result: Vec<<C as Component>::Msg> = vec![];
+        if let (Some(istate), Some(area)) = (input_state, self.layout.render_area()) {
+            if area.contains_inclusive(istate.mouse.pos) {
+                println!("cursor on button");
+                if istate.mouse.buttons.active(MouseButton::Left) {
+                    self.state = ButtonState::Clicked;
+                } else if istate.mouse.released().active(MouseButton::Left) {
+                    self.state = ButtonState::Idle;
+                    result.extend(self.hook.as_ref().map(|v| v()).flatten().into_iter());
+                } else {
+                    self.state = ButtonState::Hovered;
+                }
+            } else {
+                self.state = ButtonState::Idle;
+            }
+        }
+        result.extend(self.label.poll(uih, input_state).into_iter());
+        result
+    }
+
+    fn layout_node(&self) -> LayoutNodeAccess {
+        LayoutNodeAccess::new(self)
+    }
+    fn layout_node_mut(&mut self) -> &mut LayoutNode {
+        &mut self.layout
+    }
+
+    fn render(&self, theme: &crate::theme::Theme, target: &mut kahlo::RgbaBitmap) {
+        let colour = match self.state {
+            ButtonState::Idle => theme.background,
+            ButtonState::Hovered => theme.panel,
+            ButtonState::Clicked => theme.foreground,
+        };
+        target.fill_region(self.layout.render_area().unwrap(), colour);
+        self.label.render(theme, target);
+    }
+}

+ 18 - 7
src/widget/frame.rs

@@ -1,8 +1,10 @@
-use kahlo::{prelude::*, math::PixelSideOffsets};
+use kahlo::{math::PixelSideOffsets, prelude::*};
 
 use crate::{
     component::Component,
-    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy}, ui::UIHandle, theme::Theme,
+    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer, SizePolicy},
+    theme::Theme,
+    ui::UIHandle,
 };
 
 use super::Widget;
@@ -16,7 +18,9 @@ pub struct Frame<C: Component> {
 impl<C: Component> Frame<C> {
     pub fn new(uih: &UIHandle) -> Self {
         let mut nnode = uih.new_layout_node();
-        nnode.set_height_policy(SizePolicy::expands(1)).set_width_policy(SizePolicy::expands(1));
+        nnode
+            .set_height_policy(SizePolicy::expands(1))
+            .set_width_policy(SizePolicy::expands(1));
         Self {
             layout: nnode,
             child: None,
@@ -51,7 +55,11 @@ impl<C: Component> LayoutNodeContainer for Frame<C> {
 }
 
 impl<C: Component> Widget<C> for Frame<C> {
-    fn poll(&mut self, _uih: &mut UIHandle, _input_state: Option<&crate::input::InputState>) -> Vec<C::Msg> {
+    fn poll(
+        &mut self,
+        _uih: &mut UIHandle,
+        _input_state: Option<&crate::input::InputState>,
+    ) -> Vec<C::Msg> {
         vec![]
     }
     fn layout_node(&self) -> LayoutNodeAccess {
@@ -61,9 +69,12 @@ impl<C: Component> Widget<C> for Frame<C> {
         &mut self.layout
     }
     fn render(&self, theme: &Theme, target: &mut kahlo::RgbaBitmap) {
-        let rect = self.layout.cached_rect().unwrap();
+        let area = self.layout.render_area().unwrap();
 
-        target.fill_region(&rect.to_box2d(), theme.border);
-        target.fill_region(&rect.to_box2d().inner_box(PixelSideOffsets::new_all_same(theme.border_width as i32)), theme.panel);
+        target.fill_region(area, theme.border);
+        target.fill_region(
+            area.inner_box(PixelSideOffsets::new_all_same(theme.border_width as i32)),
+            theme.panel,
+        );
     }
 }

+ 31 - 14
src/widget/group.rs

@@ -1,15 +1,31 @@
 use crate::{
-    layout::{LayoutChildIter, LayoutNode, LayoutNodeAccess, LayoutNodeContainer},
-    platform::{render::Painter, PlatformPainter, PlatformSpec, TextInterface},
-    Component, Widget,
+    component::Component,
+    input::InputState,
+    layout::{LayoutNode, LayoutNodeAccess, LayoutNodeContainer},
+    theme::Theme,
+    ui::UIHandle,
+    widget::Widget,
 };
 
-pub struct PlainGroup<P: PlatformSpec, C: Component> {
+pub struct PlainGroup<C: Component> {
     lnode: LayoutNode,
-    children: Vec<Box<dyn Widget<P, C>>>,
+    children: Vec<Box<dyn Widget<C>>>,
 }
 
-impl<P: PlatformSpec, C: Component> LayoutNodeContainer for PlainGroup<P, C> {
+impl<C: Component> PlainGroup<C> {
+    pub fn new(uih: &UIHandle) -> Self {
+        Self {
+            lnode: uih.new_layout_node(),
+            children: vec![],
+        }
+    }
+
+    pub fn append(&mut self, widget: Box<dyn Widget<C>>) {
+        self.children.push(widget);
+    }
+}
+
+impl<C: Component> LayoutNodeContainer for PlainGroup<C> {
     fn layout_node(&self) -> &LayoutNode {
         &self.lnode
     }
@@ -23,22 +39,23 @@ impl<P: PlatformSpec, C: Component> LayoutNodeContainer for PlainGroup<P, C> {
     }
 }
 
-impl<P: PlatformSpec, C: Component> Widget<P, C> for PlainGroup<P, C> {
+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 poll(&mut self, uih: &mut UIHandle, input_state: Option<&InputState>) -> Vec<C::Msg> {
+        self.children
+            .iter_mut()
+            .map(|w| w.poll(uih, input_state))
+            .flatten()
+            .collect()
     }
-    fn render(&self, painter: &mut PlatformPainter<P>, ti: &P::Text) {
+    fn render(&self, theme: &Theme, target: &mut kahlo::RgbaBitmap) {
         for child in self.children.iter() {
-            child.render(painter, ti);
+            child.render(theme, target);
         }
     }
 }

+ 36 - 12
src/widget/label.rs

@@ -1,4 +1,6 @@
-use kahlo::{colour::Colour, math::PixelBox, prelude::*};
+use std::cell::RefCell;
+
+use kahlo::{math::PixelBox, prelude::*};
 
 use crate::{
     component::Component,
@@ -12,6 +14,7 @@ use super::Widget;
 pub struct Label<C: Component> {
     lnode: LeafLayoutNode,
     text: Option<String>,
+    rendered: RefCell<Option<kahlo::Alphamap>>,
     _ghost: std::marker::PhantomData<C>,
 }
 
@@ -27,21 +30,42 @@ impl<C: Component> Label<C> {
         Self {
             lnode: LeafLayoutNode::new(node),
             text: None,
+            rendered: None.into(),
             _ghost: Default::default(),
         }
     }
 
-    pub fn set_text(&mut self, text: String) {
-        self.text = Some(text);
+    pub fn set_text(&mut self, text: &str) {
+        if Some(text) == self.text.as_deref() {
+            return;
+        } else {
+            self.text = Some(text.to_string());
+            self.rendered.take();
+        }
+    }
+}
+
+impl<C: Component> Label<C> {
+    fn update_hints(&mut self, uih: &UIHandle) {
+        let Some(text) = self.text.as_ref() else {
+            return;
+        };
+        let line = uih.theme().make_line(text.as_str());
+        let rendered = line.render_line();
+        *self.rendered.borrow_mut() = Some(rendered);
     }
 }
 
 impl<C: Component> Widget<C> for Label<C> {
     fn poll(
         &mut self,
-        _uih: &mut UIHandle,
+        uih: &mut UIHandle,
         _input_state: Option<&crate::input::InputState>,
     ) -> Vec<<C as Component>::Msg> {
+        if self.rendered.borrow().is_none() {
+            self.update_hints(uih);
+        }
+
         vec![]
     }
 
@@ -53,16 +77,16 @@ impl<C: Component> Widget<C> for Label<C> {
     }
 
     fn render(&self, theme: &Theme, surface: &mut kahlo::RgbaBitmap) {
-        let Some(text) = self.text.as_ref() else { return };
-        let line = theme.make_line(text.as_str());
-        let rendered = line.render_line();
-
-        let render_location = self.layout_node().cached_rect().unwrap();
+        let rcon = self.rendered.borrow();
+        let Some(rendered) = rcon.as_ref() else {
+            return;
+        };
+        let render_location = self.layout_node().render_area().unwrap();
 
-        surface.fill_masked(
+        surface.fill_region_masked(
             &rendered,
-            &PixelBox::from_size(rendered.size()),
-            &render_location.origin,
+            PixelBox::from_size(rendered.size()),
+            render_location.min,
             theme.foreground,
             kahlo::colour::BlendMode::SourceAlpha,
         );

+ 53 - 13
src/window.rs

@@ -1,9 +1,12 @@
 use std::cell::RefCell;
 use std::rc::Rc;
 
-use kahlo::{math::PixelRect, math::PixelSize, prelude::*};
+use kahlo::{
+    math::{PixelBox, PixelPoint, PixelSize},
+    prelude::*,
+};
 
-use crate::input::InputState;
+use crate::input::{InputState, MouseButton};
 use crate::layout::{self, LayoutNode, LayoutNodeAccess, LinearAccess};
 use crate::widget::Widget;
 use crate::{component::Component, ui::UIHandle};
@@ -30,6 +33,7 @@ pub(crate) struct WindowState<WC: WindowComponent> {
     istate: InputState,
     draw_pending: bool,
     surface: softbuffer::Surface<Rc<winit::window::Window>, Rc<winit::window::Window>>,
+    bitmap: RefCell<Option<kahlo::RgbaBitmap>>,
 }
 
 impl<WC: WindowComponent> WindowState<WC> {
@@ -46,23 +50,47 @@ impl<WC: WindowComponent> WindowState<WC> {
         let mut buf = self.surface.buffer_mut().unwrap();
 
         self.root_node.set_behaviour(layout::NodeBehaviour::Fixed {
-            rect: PixelRect::from_size(PixelSize::new(size.width as i32, size.height as i32)),
+            area: PixelBox::from_size(PixelSize::new(size.width as i32, size.height as i32)),
         });
 
         let layout = LinearAccess::new(&self.root_node, self.wc.root_widget().layout_node());
 
-        let mut pixmap = kahlo::RgbaBitmap::new(size.width as usize, size.height as usize);
-        pixmap.fill(uih.theme().background);
-        layout::recalculate(LayoutNodeAccess::new(&layout));
-        self.wc.root_widget().render(uih.theme(), &mut pixmap);
-
-        // expensive ABGR -> RGB0 conversion
-        let mut pxdata = pixmap.data();
-        for t in buf.iter_mut() {
-            *t = u32::from_be_bytes(pxdata[0..4].try_into().unwrap()) >> 8;
-            pxdata = &pxdata[4..];
+        let mut bitmap = self.bitmap.borrow_mut();
+        let needs_recreate = match bitmap.as_mut() {
+            None => true,
+            Some(bitmap) => {
+                if bitmap.size() != PixelSize::new(size.width as i32, size.height as i32) {
+                    true
+                } else {
+                    false
+                }
+            }
+        };
+        if needs_recreate {
+            bitmap.take();
         }
 
+        let mut bitmap = bitmap.get_or_insert_with(|| {
+            kahlo::RgbaBitmap::new(size.width as usize, size.height as usize)
+        });
+
+        bitmap.fill(uih.theme().background);
+        layout::recalculate(LayoutNodeAccess::new(&layout));
+        self.wc.root_widget().render(uih.theme(), &mut bitmap);
+
+        let mut windowbuffer = kahlo::BitmapMut::<kahlo::formats::Bgr32>::new(
+            unsafe {
+                std::slice::from_raw_parts_mut(buf[..].as_mut_ptr() as *mut u8, buf.len() * 4)
+            },
+            size.width as usize,
+            size.height as usize,
+        );
+        windowbuffer.copy_from(
+            bitmap,
+            PixelBox::from_size(bitmap.size()),
+            PixelPoint::origin(),
+        );
+
         buf.present().unwrap();
     }
 
@@ -76,6 +104,8 @@ impl<WC: WindowComponent> WindowState<WC> {
         let evts = self.wc.root_widget_mut().poll(uih, Some(&self.istate));
         ret.extend(self.wc.process_all(evts.into_iter()));
 
+        self.istate.tick();
+
         if self.draw_pending {
             self.redraw(uih);
         }
@@ -87,6 +117,8 @@ impl<WC: WindowComponent> WindowState<WC> {
 pub(crate) trait WindowStateAccess {
     fn push_event(&self, we: WindowEvent);
     fn request_redraw(&self);
+    fn update_mouse_pos(&self, pos: PixelPoint);
+    fn update_mouse_button(&self, which: MouseButton, to: bool);
 }
 
 impl<WC: WindowComponent> WindowStateAccess for RefCell<WindowState<WC>> {
@@ -96,6 +128,13 @@ impl<WC: WindowComponent> WindowStateAccess for RefCell<WindowState<WC>> {
     fn request_redraw(&self) {
         self.borrow_mut().draw_pending = true;
     }
+    fn update_mouse_pos(&self, pos: PixelPoint) {
+        self.borrow_mut().istate.mouse.pos = pos;
+    }
+    fn update_mouse_button(&self, which: MouseButton, to: bool) {
+        let is = &mut self.borrow_mut().istate;
+        is.mouse.buttons.set_button(which, to);
+    }
 }
 
 pub struct WindowBuilder<'r, 'l: 'r> {
@@ -128,6 +167,7 @@ impl<'r, 'l: 'r> WindowBuilder<'r, 'l> {
             istate: InputState::default().into(),
             draw_pending: false.into(),
             surface: surface.into(),
+            bitmap: None.into(),
         }));
 
         self.ui_handle.state.window_states.insert(