Pārlūkot izejas kodu

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

Kestrel 11 mēneši atpakaļ
vecāks
revīzija
b88087436f
16 mainītis faili ar 495 papildinājumiem un 141 dzēšanām
  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(