Explorar o código

Rearrange and sketch out widget and component framework.

Kestrel hai 5 meses
pai
achega
e9a5022ac2

+ 1 - 0
.gitignore

@@ -1,2 +1,3 @@
 /target
+/Cargo.lock
 .*.swp

+ 1 - 0
.vimrc

@@ -0,0 +1 @@
+set wildignore+=target

+ 3 - 10
Cargo.toml

@@ -1,10 +1,3 @@
-[package]
-name = "patina"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-smallvec = "1.13"
-log = "0.4"
+[workspace]
+resolver = "2"
+members = ["patina", "patina-sdl"]

+ 10 - 0
patina-sdl/Cargo.toml

@@ -0,0 +1,10 @@
+[package]
+name = "patina-sdl"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+patina = { path = "../patina/" }
+sdl2 = { version = "0.36", features = ["ttf"] }

+ 85 - 0
patina-sdl/src/lib.rs

@@ -0,0 +1,85 @@
+use patina::platform;
+
+pub struct SDLInitFlags {
+    pub window_width: usize,
+    pub window_height: usize,
+    pub window_resizable: bool,
+}
+
+impl Default for SDLInitFlags {
+    fn default() -> Self {
+        Self {
+            window_width: 800,
+            window_height: 600,
+            window_resizable: false,
+        }
+    }
+}
+
+pub struct SDLRenderInterface {
+    sdl: sdl2::Sdl,
+    video: sdl2::VideoSubsystem,
+    window: sdl2::video::Window,
+}
+
+impl SDLRenderInterface {
+    pub fn new(sdlif: SDLInitFlags) -> (Self, SDLEventInterface) {
+        let sdl = sdl2::init().unwrap();
+        let evt = sdl.event_pump().unwrap();
+        let video = sdl.video().unwrap();
+
+        let window = video
+            .window(
+                "title",
+                sdlif.window_width as u32,
+                sdlif.window_height as u32,
+            )
+            .position_centered()
+            .build()
+            .unwrap();
+
+        (Self { sdl, video, window }, SDLEventInterface { evt, next: None })
+    }
+}
+
+impl platform::RenderInterface for SDLRenderInterface {}
+
+pub struct SDLEventInterface {
+    evt: sdl2::EventPump,
+    next: Option<sdl2::event::Event>,
+}
+
+impl SDLEventInterface {
+    fn translate(&self, sdlevent: sdl2::event::Event) -> Option<platform::event::WindowEvent> {
+        match sdlevent {
+            sdl2::event::Event::Quit { .. } => Some(platform::event::WindowEvent::CloseRequest),
+            _ => None,
+        }
+    }
+}
+
+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)
+        }
+    }
+    fn wait_event(&mut self) {
+        if self.next.is_some() { return }
+        self.next = Some(self.evt.wait_event());
+    }
+}
+
+pub struct SDLTextInterface {}
+
+impl platform::TextInterface for SDLTextInterface {}
+
+pub fn init_sdl_platform(
+    sdlif: SDLInitFlags,
+) -> (SDLRenderInterface, SDLEventInterface, SDLTextInterface) {
+    let (sdl, event) = SDLRenderInterface::new(sdlif);
+    (sdl, event, SDLTextInterface {})
+}

+ 78 - 0
patina/Cargo.lock

@@ -0,0 +1,78 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.154"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "patina"
+version = "0.1.0"
+dependencies = [
+ "log",
+ "patina-sdl",
+]
+
+[[package]]
+name = "patina-sdl"
+version = "0.1.0"
+dependencies = [
+ "patina",
+ "sdl2",
+]
+
+[[package]]
+name = "sdl2"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8356b2697d1ead5a34f40bcc3c5d3620205fe0c7be0a14656223bfeec0258891"
+dependencies = [
+ "bitflags",
+ "lazy_static",
+ "libc",
+ "sdl2-sys",
+]
+
+[[package]]
+name = "sdl2-sys"
+version = "0.36.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26bcacfdd45d539fb5785049feb0038a63931aa896c7763a2a12e125ec58bd29"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "version-compare",
+]
+
+[[package]]
+name = "version-compare"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"

+ 12 - 0
patina/Cargo.toml

@@ -0,0 +1,12 @@
+[package]
+name = "patina"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+log = "0.4"
+
+[dev-dependencies]
+patina-sdl = { path = "../patina-sdl/" }

+ 10 - 0
patina/examples/sdl_plain.rs

@@ -0,0 +1,10 @@
+use patina::prelude::*;
+// use patina_sdl::
+
+fn main() {
+    let ui = patina::UI::new(patina_sdl::init_sdl_platform(patina_sdl::SDLInitFlags {
+        ..Default::default()
+    }));
+
+    ui.run();
+}

+ 123 - 0
patina/src/component.rs

@@ -0,0 +1,123 @@
+use std::{cell::RefCell, rc::Rc};
+
+pub struct ComponentStorage<Msg> {
+    queue: RefCell<Vec<Msg>>,
+}
+
+impl<Msg> Default for ComponentStorage<Msg> {
+    fn default() -> Self {
+        Self {
+            queue: vec![].into()
+        }
+    }
+}
+
+impl<Msg> ComponentStorage<Msg> {
+    pub fn enqueue(&self, msg: Msg) {
+        self.queue.borrow_mut().push(msg);
+    }
+}
+
+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>;
+}
+
+/*
+pub mod counter_component_example {
+    use std::rc::Weak;
+
+    use super::*;
+    use crate::widget::Widget;
+
+    pub enum CounterMsg {
+        Increase,
+        Decrease,
+        Reset
+    }
+
+    struct ButtonWidget<C: Component> {
+        click_mapper: Option<Box<dyn Fn() -> Option<C::Msg>>>,
+        _ghost: std::marker::PhantomData<C>,
+    }
+
+    impl<C: Component> ButtonWidget<C> {
+        pub fn new(_cstore: Weak<ComponentStorage<C::Msg>>, click_mapper: impl Fn() -> Option<C::Msg> + 'static) -> Self {
+            Self {
+                click_mapper: Some(Box::new(click_mapper)),
+                _ghost: Default::default(),
+            }
+        }
+    }
+
+    impl<C: Component> Widget<C> for ButtonWidget<C> {
+        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 = <C as Component>::Msg> {
+            // for now, simulate a click every time
+            self.click_mapper.as_ref().map(|v| (v)()).flatten().into_iter()
+        }
+    }
+
+    pub struct CounterComponent<PC: Component> {
+        val: usize,
+        inc: ButtonWidget<Self>,
+        dec: ButtonWidget<Self>,
+        cstore: Rc<ComponentStorage<CounterMsg>>,
+        upd_mapper: Box<dyn Fn(usize) -> Option<PC::Msg>>,
+    }
+
+    impl<PC: Component> CounterComponent<PC> {
+        pub fn new(_pc: &PC, upd_mapper: impl Fn(usize) -> Option<PC::Msg> + 'static) -> Self {
+            // let pcstore = Rc::downgrade(&pc.storage());
+            let cstore = Default::default();
+            Self {
+                val: 0,
+                inc: ButtonWidget::new(Rc::downgrade(&cstore), || Some(CounterMsg::Increase)),
+                dec: ButtonWidget::new(Rc::downgrade(&cstore), || Some(CounterMsg::Decrease)),
+                cstore,
+                upd_mapper: Box::new(upd_mapper),
+            }
+        }
+    }
+
+    impl<PC: Component> Component for CounterComponent<PC> {
+        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> {
+            match msg {
+                CounterMsg::Increase => self.val += 1,
+                CounterMsg::Decrease => self.val -= 1,
+                CounterMsg::Reset => self.val = 0,
+            }
+            (self.upd_mapper)(self.val)
+        }
+    }
+
+    impl<PC: Component> Widget<PC> for CounterComponent<PC> {
+        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()
+        }
+    }
+}
+*/

+ 0 - 0
src/geom.rs → patina/src/geom.rs


+ 1 - 0
patina/src/input.rs

@@ -0,0 +1 @@
+pub struct InputState;

+ 2 - 1
src/layout.rs → patina/src/layout.rs

@@ -3,13 +3,14 @@ use std::{
     rc::Rc,
 };
 
-use cache::{Layer, LayoutCache, LayoutCacheKey, NodeState};
 use crate::geom::IRect;
+use cache::{Layer, LayoutCacheKey, NodeState};
 
 mod arr;
 mod cache;
 mod calc;
 
+pub use cache::LayoutCache;
 pub use calc::recalculate;
 
 /// Sizing policy for a layout dimension

+ 45 - 11
src/layout/arr.rs → patina/src/layout/arr.rs

@@ -41,12 +41,20 @@ impl ArrangementCalculator for LineArrangement {
         let cw_policy = child_policies
             .iter()
             .map(|v| v.0)
-            .reduce(if self.is_column() { SizePolicy::max } else { SizePolicy::add })
+            .reduce(if self.is_column() {
+                SizePolicy::max
+            } else {
+                SizePolicy::add
+            })
             .unwrap();
         let ch_policy: SizePolicy = child_policies
             .iter()
             .map(|v| v.1)
-            .reduce(if self.is_column() { SizePolicy::add } else { SizePolicy::max })
+            .reduce(if self.is_column() {
+                SizePolicy::add
+            } else {
+                SizePolicy::max
+            })
             .unwrap();
         (
             cw_policy.max_preserve_slack(node.width_policy),
@@ -66,18 +74,41 @@ impl ArrangementCalculator for LineArrangement {
             |ns: &mut NodeState| ns.net_policy.0
         };
 
-        let policies = node.child_iter().map(|c| node.cache.with_state(c.cache_key, ee).unwrap());
+        let policies = node
+            .child_iter()
+            .map(|c| node.cache.with_state(c.cache_key, ee).unwrap());
         let mut last_offset = 0;
-        for (offset, child) in do_fit(if self.is_column() { inside.height() } else { inside.width() }, policies).zip(node.child_iter()) {
+        for (offset, child) in do_fit(
+            if self.is_column() {
+                inside.height()
+            } else {
+                inside.width()
+            },
+            policies,
+        )
+        .zip(node.child_iter())
+        {
             let crect = if self.is_column() {
                 IRect::new_from_size(
-                    IPoint { x: inside.base().x, y: inside.ul().y + last_offset },
-                    IVector { x: inside.width(), y: (offset - last_offset) }
+                    IPoint {
+                        x: inside.base().x,
+                        y: inside.ul().y + last_offset,
+                    },
+                    IVector {
+                        x: inside.width(),
+                        y: (offset - last_offset),
+                    },
                 )
             } else {
                 IRect::new_from_size(
-                    IPoint { x: inside.ul().x + last_offset, y: inside.base().y },
-                    IVector { x: (offset - last_offset), y: inside.height() }
+                    IPoint {
+                        x: inside.ul().x + last_offset,
+                        y: inside.base().y,
+                    },
+                    IVector {
+                        x: (offset - last_offset),
+                        y: inside.height(),
+                    },
                 )
             };
 
@@ -87,13 +118,16 @@ impl ArrangementCalculator for LineArrangement {
     }
 }
 
-fn do_fit<'l>(total: i32, policies: impl Iterator<Item = SizePolicy> + Clone) -> impl Iterator<Item = i32> {
+fn do_fit<'l>(
+    total: i32,
+    policies: impl Iterator<Item = SizePolicy> + Clone,
+) -> impl Iterator<Item = i32> {
     // 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();
 
     // 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 = (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
 
     // how much slack space do we have left?

+ 0 - 0
src/layout/cache.rs → patina/src/layout/cache.rs


+ 0 - 0
src/layout/calc.rs → patina/src/layout/calc.rs


+ 49 - 0
patina/src/lib.rs

@@ -0,0 +1,49 @@
+use platform::EventInterface;
+
+pub mod geom;
+pub mod layout;
+pub mod platform;
+pub mod component;
+pub mod widget;
+pub mod input;
+
+pub mod prelude {
+
+}
+
+pub struct UI<PT: platform::PlatformTuple> {
+    layout_cache: std::rc::Rc<layout::LayoutCache>,
+    platform: PT,
+}
+
+impl<PT: platform::PlatformTuple> UI<PT> {
+    pub fn new(pt: PT) -> Self {
+        Self {
+            layout_cache: layout::LayoutCache::new(),
+            platform: pt,
+        }
+    }
+
+    pub fn update(&mut self) {
+        while let Some(evt) = self.platform.event_mut().poll_event() {
+            match evt {
+                platform::event::WindowEvent::CloseRequest => {
+                    std::process::exit(0);
+                }
+                _ => {},
+            }
+        }
+    }
+
+    pub fn sink_event(&mut self, evt: platform::event::UIEvent) {
+        
+    }
+
+    pub fn run(mut self) {
+        loop {
+            self.update();
+
+            self.platform.event_mut().wait_event();
+        }
+    }
+}

+ 54 - 0
patina/src/platform.rs

@@ -0,0 +1,54 @@
+pub mod render;
+
+pub trait RenderInterface {}
+
+pub mod event;
+
+pub trait EventInterface {
+    fn poll_event(&mut self) -> Option<event::WindowEvent>;
+    fn wait_event(&mut self);
+}
+
+pub trait TextInterface {}
+
+pub trait PlatformTuple {
+    type Render: RenderInterface;
+    type Event: EventInterface;
+    type Text: TextInterface;
+
+    fn render(&self) -> &Self::Render;
+    fn render_mut(&mut self) -> &mut Self::Render;
+
+    fn event(&self) -> &Self::Event;
+    fn event_mut(&mut self) -> &mut Self::Event;
+
+    fn text(&self) -> &Self::Text;
+    fn text_mut(&mut self) -> &mut Self::Text;
+}
+
+impl<RI: RenderInterface, EI: EventInterface, TI: TextInterface> PlatformTuple for (RI, EI, TI) {
+    type Render = RI;
+    type Event = EI;
+    type Text = TI;
+
+    fn render(&self) -> &Self::Render {
+        &self.0
+    }
+    fn render_mut(&mut self) -> &mut Self::Render {
+        &mut self.0
+    }
+
+    fn event(&self) -> &Self::Event {
+        &self.1
+    }
+    fn event_mut(&mut self) -> &mut Self::Event {
+        &mut self.1
+    }
+
+    fn text(&self) -> &Self::Text {
+        &self.2
+    }
+    fn text_mut(&mut self) -> &mut Self::Text {
+        &mut self.2
+    }
+}

+ 35 - 0
patina/src/platform/event.rs

@@ -0,0 +1,35 @@
+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)
+    }
+}
+
+#[derive(Clone, Debug)]
+pub enum UIEvent {
+    MouseUpdate,
+    KeyDown,
+    KeyUp,
+    TextInput,
+}
+
+#[derive(Clone, Debug)]
+pub enum WindowEvent {
+    RepaintRequest,
+    Resize,
+    CloseRequest,
+}

+ 66 - 0
patina/src/platform/render.rs

@@ -0,0 +1,66 @@
+#[derive(Clone, Copy, Debug)]
+pub enum SurfaceFormat {
+    RGBA,
+    ARGB,
+    ABGR,
+    RGB,
+    GBR,
+}
+
+impl SurfaceFormat {
+    pub fn stride(&self) -> usize {
+        match self {
+            Self::RGBA | Self::ARGB | Self::ABGR => 4,
+            Self::RGB | Self::GBR => 3,
+        }
+    }
+
+    /// 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!(),
+        }
+    }
+
+    /// 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!(),
+        }
+    }
+
+    /// 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!(),
+        }
+    }
+
+    pub const fn alpha_mask(&self) -> (u8, u8) {
+        match self {
+            Self::RGBA => (24, 8),
+            Self::RGB => (0, 0),
+            _ => todo!(),
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub enum AccessOrder {
+    RowMajor,
+    ColumnMajor,
+}
+
+pub trait Surface {
+    fn width(&self) -> usize;
+    fn height(&self) -> usize;
+
+    fn access(&self) -> AccessOrder;
+    fn format(&self) -> SurfaceFormat;
+}

+ 1 - 0
patina/src/surface.rs

@@ -0,0 +1 @@
+

+ 21 - 0
patina/src/widget.rs

@@ -0,0 +1,21 @@
+use std::rc::{Rc, Weak};
+
+use crate::{component::Component, input::InputState, layout::LayoutNodeAccess};
+
+#[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)
+        )
+    }
+}
+
+pub trait Widget<C: Component>: 'static {
+    fn poll(&mut self, input_state: &InputState) -> impl Iterator<Item = C::Msg>;
+
+    fn layout_node(&self) -> &dyn LayoutNodeAccess;
+    fn render(&self, surface: ());
+}

+ 80 - 0
patina/src/widget/context.rs

@@ -0,0 +1,80 @@
+use std::collections::HashMap;
+
+use super::{Widget, WidgetID};
+
+#[derive(Default)]
+pub struct WidgetContext<Message> {
+    mappers: HashMap<WidgetID, Box<dyn std::any::Any>>,
+    queue: Vec<Message>,
+    _ghost: std::marker::PhantomData<Message>,
+}
+
+impl<Message: 'static> WidgetContext<Message> {
+    pub fn register<W: Widget>(&mut self, id: WidgetID, cb: impl Fn(W::Event) -> Option<Message> + 'static) {
+        let mapper = Box::new(cb) as Box<dyn Fn(W::Event) -> Option<Message>>;
+        self.mappers.insert(id, Box::new(mapper));
+    }
+    pub fn clear(&mut self, id: WidgetID) {
+        self.mappers.remove(&id);
+    }
+}
+
+pub trait WidgetContextAccess<Event> {
+    fn sink(&mut self, id: WidgetID, event: Event);
+}
+
+impl<Event: 'static, Message: 'static> WidgetContextAccess<Event> for WidgetContext<Message> {
+    fn sink(&mut self, id: WidgetID, event: Event) {
+        let Some(mapper) = self.mappers.get(&id) else {
+            return
+        };
+
+        let mapper = mapper.downcast_ref::<Box<dyn Fn(Event) -> Option<Message>>>().expect("invalid mapper");
+
+        self.queue.extend((*mapper)(event).into_iter());
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::widget::{Widget, WidgetID};
+
+    use super::WidgetContext;
+
+    struct SimpleWidget(WidgetID);
+    impl Widget for SimpleWidget {
+        type Event = usize;
+
+        fn id(&self) -> crate::widget::WidgetID { self.0 }
+        fn layout_node(&self) -> &dyn crate::layout::LayoutNodeAccess {
+            todo!()
+        }
+        fn poll(&self, _input_info: (), comp: &mut dyn super::WidgetContextAccess<Self::Event>) {
+            comp.sink(self.id(), 42);
+        }
+        fn render(&self, _surface: ()) {
+            todo!()
+        }
+    }
+
+    #[test]
+    fn sink_test() {
+        let mut wcon = WidgetContext::<()>::default();
+
+        let id1 = WidgetID::new();
+        let sw1 = SimpleWidget(id1);
+
+        let id2 = WidgetID::new();
+        let sw2 = SimpleWidget(id2);
+
+        wcon.register::<SimpleWidget>(id2, |v| {
+            println!("cb: {v}");
+            Some(())
+        });
+
+        sw1.poll((), &mut wcon);
+        sw2.poll((), &mut wcon);
+
+        assert_eq!(wcon.queue, vec![()]);
+    }
+}

+ 0 - 2
src/lib.rs

@@ -1,2 +0,0 @@
-pub mod geom;
-pub mod layout;