reactivity base

This commit is contained in:
2025-08-10 02:05:35 -04:00
parent 56beeb80f3
commit 58d9b32077
12 changed files with 394 additions and 328 deletions

View File

@@ -2,7 +2,7 @@ use std::ops::Range;
use crate::{
primitive::{Axis, Painter, RoundedRectData, UIRegion},
NodeArray, UIColor, UINode,
UIColor, Widget, WidgetArrLike, WidgetFn, WidgetId, WidgetLike,
};
#[derive(Clone, Copy)]
@@ -20,7 +20,7 @@ impl RoundedRect {
}
}
impl UINode for RoundedRect {
impl Widget for RoundedRect {
fn draw(&self, painter: &mut Painter) {
painter.write(RoundedRectData {
color: self.color,
@@ -32,11 +32,11 @@ impl UINode for RoundedRect {
}
pub struct Span {
pub elements: Vec<(Range<f32>, Box<dyn UINode>)>,
pub elements: Vec<(Range<f32>, WidgetId)>,
pub axis: Axis,
}
impl UINode for Span {
impl Widget for Span {
fn draw(&self, painter: &mut Painter) {
for (span, child) in &self.elements {
let mut sub_region = UIRegion::full();
@@ -52,14 +52,13 @@ impl Span {
pub fn proportioned<const LEN: usize>(
axis: Axis,
ratios: [impl UINum; LEN],
elements: impl NodeArray<LEN>,
elements: [WidgetId; LEN],
) -> Self {
let ratios = ratios.map(|r| r.to_f32());
let total: f32 = ratios.iter().sum();
let mut start = 0.0;
Self {
elements: elements
.to_arr()
.into_iter()
.zip(ratios)
.map(|(e, r)| {
@@ -74,15 +73,15 @@ impl Span {
}
}
pub struct Regioned<N: UINode> {
pub struct Regioned {
region: UIRegion,
inner: N,
inner: WidgetId,
}
impl<N: UINode> UINode for Regioned<N> {
impl Widget for Regioned {
fn draw(&self, painter: &mut Painter) {
painter.region.select(&self.region);
self.inner.draw(painter);
painter.draw(&self.inner);
}
}
@@ -118,26 +117,26 @@ impl<T: UINum> From<T> for Padding {
}
}
pub trait NodeUtil: Sized + UINode {
fn pad(self, padding: impl Into<Padding>) -> Regioned<Self>;
pub trait WidgetUtil<W> {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetLike<Regioned>;
}
impl<T: UINode> NodeUtil for T {
fn pad(self, padding: impl Into<Padding>) -> Regioned<Self> {
Regioned {
impl<W: Widget, WL: WidgetLike<W>> WidgetUtil<W> for WL {
fn pad(self, padding: impl Into<Padding>) -> impl WidgetLike<Regioned> {
WidgetFn(|ui| Regioned {
region: padding.into().region(),
inner: self,
}
inner: self.id(ui).erase_type(),
})
}
}
pub trait NodeArrayUtil<const LEN: usize> {
fn proportioned(self, axis: Axis, ratios: [impl UINum; LEN]) -> Span;
pub trait WidgetArrUtil<const LEN: usize, Ws> {
fn span(self, axis: Axis, ratios: [impl UINum; LEN]) -> impl WidgetLike<Span>;
}
impl<T: NodeArray<LEN>, const LEN: usize> NodeArrayUtil<LEN> for T {
fn proportioned(self, axis: Axis, ratios: [impl UINum; LEN]) -> Span {
Span::proportioned(axis, ratios, self)
impl<const LEN: usize, Ws, Wa: WidgetArrLike<LEN, Ws>> WidgetArrUtil<LEN, Ws> for Wa {
fn span(self, axis: Axis, ratios: [impl UINum; LEN]) -> impl WidgetLike<Span> {
WidgetFn(move |ui| Span::proportioned(axis, ratios, self.ui(ui).arr))
}
}

View File

@@ -1,66 +1,8 @@
mod node;
pub use node::*;
mod ui;
mod widget;
use crate::primitive::{Color, Painter};
pub use ui::*;
pub use widget::*;
use crate::primitive::Color;
pub type UIColor = Color<u8>;
pub trait UINode: 'static {
fn draw(&self, painter: &mut Painter);
}
pub struct UI {
base: Box<dyn UINode>,
}
impl UI {
pub fn to_primitives(&self) -> Painter {
let mut painter = Painter::default();
self.base.draw(&mut painter);
painter
}
}
impl<N: UINode> From<N> for UI {
fn from(node: N) -> Self {
Self {
base: Box::new(node),
}
}
}
impl UINode for Box<dyn UINode> {
fn draw(&self, painter: &mut Painter) {
self.as_ref().draw(painter);
}
}
pub trait NodeArray<const LEN: usize> {
fn to_arr(self) -> [Box<dyn UINode>; LEN];
}
// I hate this language it's so bad why do I even use it
macro_rules! impl_node_arr {
($n:expr;$($T:tt)+) => {
impl<$($T: UINode,)*> NodeArray<$n> for ($($T,)*) {
fn to_arr(self) -> [Box<dyn UINode>; $n] {
#[allow(non_snake_case)]
let ($($T,)*) = self;
[$(Box::new($T),)*]
}
}
};
}
impl_node_arr!(1;A);
impl_node_arr!(2;A B);
impl_node_arr!(3;A B C);
impl_node_arr!(4;A B C D);
impl_node_arr!(5;A B C D E);
impl_node_arr!(6;A B C D E F);
impl_node_arr!(7;A B C D E F G);
impl_node_arr!(8;A B C D E F G H);
impl_node_arr!(9;A B C D E F G H I);
impl_node_arr!(10;A B C D E F G H I J);
impl_node_arr!(11;A B C D E F G H I J K);
impl_node_arr!(12;A B C D E F G H I J K L);

101
src/layout/ui.rs Normal file
View File

@@ -0,0 +1,101 @@
use crate::{
primitive::{Painter, Primitives},
util::{IDTracker, ID},
HashMap, Widget, WidgetId, WidgetLike, WidgetRef,
};
use std::{
any::{Any, TypeId},
cell::RefCell,
rc::Rc,
};
pub struct UI {
ids: IDTracker,
base: Option<WidgetId>,
pub widgets: Widgets,
}
pub struct Widgets(HashMap<ID, Box<dyn Widget>>);
#[derive(Clone)]
pub struct UIBuilder {
ui: Rc<RefCell<UI>>,
}
impl From<UI> for UIBuilder {
fn from(ui: UI) -> Self {
UIBuilder {
ui: Rc::new(RefCell::new(ui)),
}
}
}
impl UIBuilder {
pub fn add<W: Widget>(&mut self, w: W) -> WidgetRef<W> {
WidgetRef::new(self.clone(), [self.push(w)])
}
pub fn push<W: Widget>(&mut self, w: W) -> WidgetId {
let mut ui = self.ui.borrow_mut();
let id = ui.ids.next();
ui.widgets.insert(id.duplicate(), w);
WidgetId::new(id, TypeId::of::<W>())
}
pub fn finish<WL: WidgetLike<W>, W>(mut self, base: WL) -> UI {
let base = base.id(&mut self).erase_type();
let mut ui = Rc::into_inner(self.ui).unwrap().into_inner();
ui.base = Some(base);
ui
}
}
impl UI {
pub fn build() -> UIBuilder {
Self::empty().into()
}
pub fn empty() -> Self {
Self {
ids: IDTracker::new(),
base: None,
widgets: Widgets::new(),
}
}
pub fn to_primitives(&self) -> Primitives {
let mut painter = Painter::new(&self.widgets);
if let Some(base) = &self.base {
painter.draw(base);
}
painter.finish()
}
}
impl Widgets {
fn new() -> Self {
Self(HashMap::new())
}
pub fn get(&self, id: &WidgetId) -> &dyn Widget {
self.0.get(&id.id).unwrap().as_ref()
}
pub fn get_mut<W: Widget>(&mut self, id: &WidgetId<W>) -> Option<&mut W> {
self.0.get_mut(&id.id).unwrap().as_any_mut().downcast_mut()
}
pub fn insert(&mut self, id: ID, widget: impl Widget) {
self.0.insert(id, Box::new(widget));
}
pub fn insert_any(&mut self, id: ID, widget: Box<dyn Widget>) {
self.0.insert(id, widget);
}
}
impl dyn Widget {
pub fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}

157
src/layout/widget.rs Normal file
View File

@@ -0,0 +1,157 @@
use std::{
any::{Any, TypeId},
marker::PhantomData,
};
use crate::{primitive::Painter, util::ID, UIBuilder};
pub trait Widget: 'static + Any {
fn draw(&self, painter: &mut Painter);
}
impl<W: Widget> Widget for (W,) {
fn draw(&self, painter: &mut Painter) {
self.0.draw(painter);
}
}
#[derive(Eq, Hash, PartialEq, Debug)]
pub struct WidgetId<W = ()> {
pub(super) ty: TypeId,
pub(super) id: ID,
_pd: PhantomData<W>,
}
// TODO: temp
impl Clone for WidgetId {
fn clone(&self) -> Self {
Self {
ty: self.ty,
id: self.id.duplicate(),
_pd: self._pd,
}
}
}
impl<W> WidgetId<W> {
pub(super) fn new(id: ID, ty: TypeId) -> Self {
Self {
ty,
id,
_pd: PhantomData,
}
}
pub fn erase_type(self) -> WidgetId<()> {
self.cast_type()
}
fn cast_type<W2>(self) -> WidgetId<W2> {
WidgetId {
ty: self.ty,
id: self.id,
_pd: PhantomData,
}
}
}
pub trait WidgetLike<W> {
fn id(self, ui: &mut UIBuilder) -> WidgetId<W>;
}
/// wouldn't be needed if negative trait bounds & disjoint impls existed
pub struct WidgetFn<F: FnOnce(&mut UIBuilder) -> W, W>(pub F);
impl<W: Widget, F: FnOnce(&mut UIBuilder) -> W> WidgetLike<W> for WidgetFn<F, W> {
fn id(self, ui: &mut UIBuilder) -> WidgetId<W> {
let w = (self.0)(ui);
ui.add(w).to_id()
}
}
impl<W: Widget> WidgetLike<W> for W {
fn id(self, ui: &mut UIBuilder) -> WidgetId<W> {
ui.add(self).to_id()
}
}
impl<W> WidgetLike<W> for WidgetId<W> {
fn id(self, _: &mut UIBuilder) -> WidgetId<W> {
self
}
}
impl<W> WidgetLike<W> for WidgetArr<1, (W,)> {
fn id(self, _: &mut UIBuilder) -> WidgetId<W> {
let [id] = self.arr;
id.cast_type()
}
}
pub struct WidgetArr<const LEN: usize, Ws> {
pub ui: UIBuilder,
pub arr: [WidgetId<()>; LEN],
_pd: PhantomData<Ws>,
}
impl<const LEN: usize, Ws> WidgetArr<LEN, Ws> {
pub fn new(ui: UIBuilder, arr: [WidgetId<()>; LEN]) -> Self {
Self {
ui,
arr,
_pd: PhantomData,
}
}
}
pub type WidgetRef<W> = WidgetArr<1, (W,)>;
impl<W> WidgetRef<W> {
pub fn handle(&self) -> WidgetId<W> {
let [id] = &self.arr;
id.clone().cast_type()
}
pub fn to_id(self) -> WidgetId<W> {
let [id] = self.arr;
id.cast_type()
}
}
pub trait WidgetArrLike<const LEN: usize, Ws> {
fn ui(self, ui: &mut UIBuilder) -> WidgetArr<LEN, Ws>;
}
impl<const LEN: usize, Ws> WidgetArrLike<LEN, Ws> for WidgetArr<LEN, Ws> {
fn ui(self, _: &mut UIBuilder) -> WidgetArr<LEN, Ws> {
self
}
}
// I hate this language it's so bad why do I even use it
macro_rules! impl_node_arr {
($n:expr;$($T:tt)*) => {
impl<$($T,${concat($T,$T)}: WidgetLike<$T>,)*> WidgetArrLike<$n, ($($T,)*)> for ($(${concat($T,$T)},)*) {
#[allow(unused_variables)]
fn ui(self, ui: &mut UIBuilder) -> WidgetArr<$n, ($($T,)*)> {
#[allow(non_snake_case)]
let ($($T,)*) = self;
WidgetArr::new(
ui.clone(),
[$($T.id(ui).cast_type(),)*],
)
}
}
};
}
impl_node_arr!(1;A);
impl_node_arr!(2;A B);
impl_node_arr!(3;A B C);
impl_node_arr!(4;A B C D);
impl_node_arr!(5;A B C D E);
impl_node_arr!(6;A B C D E F);
impl_node_arr!(7;A B C D E F G);
impl_node_arr!(8;A B C D E F G H);
impl_node_arr!(9;A B C D E F G H I);
impl_node_arr!(10;A B C D E F G H I J);
impl_node_arr!(11;A B C D E F G H I J K);
impl_node_arr!(12;A B C D E F G H I J K L);

View File

@@ -2,9 +2,15 @@
#![feature(const_ops)]
#![feature(const_trait_impl)]
#![feature(const_from)]
#![feature(trait_alias)]
mod layout;
mod render;
mod util;
mod base;
pub use layout::*;
pub use render::*;
pub use base::*;
pub type HashMap<K, V> = std::collections::HashMap<K, V>;

View File

@@ -8,23 +8,19 @@ pub use def::*;
pub use format::*;
pub use point::*;
use crate::{render::data::PrimitiveInstance, UINode};
use crate::{render::data::PrimitiveInstance, WidgetId, Widgets};
use bytemuck::Pod;
pub struct Painter {
pub region: UIRegion,
#[derive(Default)]
pub struct Primitives {
pub instances: Vec<PrimitiveInstance>,
pub data: Vec<u32>,
}
impl Default for Painter {
fn default() -> Self {
Self {
region: UIRegion::full(),
instances: Default::default(),
data: Default::default(),
}
}
pub struct Painter<'a> {
nodes: &'a Widgets,
primitives: Primitives,
pub region: UIRegion,
}
/// NOTE: Self must have at least u32 alignment
@@ -32,19 +28,36 @@ pub trait PrimitiveData: Pod {
const DISCRIM: u32;
}
impl Painter {
pub fn write<D: PrimitiveData>(&mut self, data: D) {
let ptr = self.data.len() as u32;
impl<'a> Painter<'a> {
pub fn new(nodes: &'a Widgets) -> Self {
Self {
nodes,
primitives: Primitives::default(),
region: UIRegion::full(),
}
}
pub fn write<Data: PrimitiveData>(&mut self, data: Data) {
let ptr = self.primitives.data.len() as u32;
let region = self.region;
self.instances.push(PrimitiveInstance { region, ptr });
self.data.push(D::DISCRIM);
self.data
self.primitives
.instances
.push(PrimitiveInstance { region, ptr });
self.primitives.data.push(Data::DISCRIM);
self.primitives
.data
.extend_from_slice(bytemuck::cast_slice::<_, u32>(&[data]));
}
pub fn draw_within(&mut self, node: &impl UINode, region: UIRegion) {
pub fn draw(&mut self, node: &WidgetId) {
self.nodes.get(node).draw(self);
}
pub fn draw_within(&mut self, node: &WidgetId, region: UIRegion) {
let old = self.region;
self.region.select(&region);
node.draw(self);
self.draw(node);
self.region = old;
}
pub fn finish(self) -> Primitives {
self.primitives
}
}

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use app::App;
use gui::{primitive::Axis, NodeArrayUtil, NodeUtil, RoundedRect, UIColor};
use gui::{primitive::Axis, RoundedRect, UIColor, WidgetArrLike, WidgetArrUtil, WidgetUtil, UI};
use render::Renderer;
use winit::{event::WindowEvent, event_loop::ActiveEventLoop, window::Window};
@@ -25,24 +25,32 @@ impl Client {
thickness: 0.0,
inner_radius: 0.0,
};
let ui = (
let mut ui = UI::build();
let blue = ui.add(rect.color(UIColor::BLUE));
let handle = blue.handle();
let mut ui = ui.finish(
(
rect.color(UIColor::BLUE),
(
rect.color(UIColor::RED),
(rect.color(UIColor::ORANGE), rect.color(UIColor::LIME))
.proportioned(Axis::Y, [1, 1]),
rect.color(UIColor::YELLOW),
blue,
(
rect.color(UIColor::RED),
(
rect.color(UIColor::ORANGE),
rect.color(UIColor::LIME).pad(10.0),
)
.span(Axis::Y, [1, 1]),
rect.color(UIColor::YELLOW),
)
.span(Axis::X, [2, 2, 1])
.pad(10),
)
.proportioned(Axis::X, [2, 2, 1])
.pad(10),
.span(Axis::X, [1, 3]),
rect.color(UIColor::GREEN),
)
.proportioned(Axis::X, [1, 3]),
rect.color(UIColor::GREEN),
)
.proportioned(Axis::Y, [3, 1])
.pad(10)
.into();
.span(Axis::Y, [3, 1])
.pad(10),
);
ui.widgets.get_mut(&handle).unwrap().color = UIColor::MAGENTA;
renderer.update(&ui);
Self { renderer }
}

43
src/util/id.rs Normal file
View File

@@ -0,0 +1,43 @@
/// intentionally does not implement copy or clone
/// which should make it harder to misuse;
/// the idea is to generally try to guarantee all IDs
/// point to something valid, although duplicate
/// gets around this if needed
#[derive(Eq, Hash, PartialEq, Debug)]
pub struct ID(usize);
#[derive(Default)]
pub struct IDTracker {
free: Vec<ID>,
cur: usize,
}
impl IDTracker {
pub fn new() -> Self {
Self::default()
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> ID {
if let Some(id) = self.free.pop() {
return id;
}
let id = ID(self.cur);
self.cur += 1;
id
}
pub fn free(&mut self, id: ID) {
self.free.push(id);
}
}
impl ID {
/// this must be used carefully to make sure
/// all IDs are still valid references;
/// named weirdly to indicate this.
/// generally should not be used in "user" code
pub fn duplicate(&self) -> Self {
Self(self.0)
}
}

3
src/util/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
mod id;
pub use id::*;