5 Commits

Author SHA1 Message Date
7b54aaf3c4 readme 2026-01-29 16:39:19 -05:00
17c436d944 stuff 2026-01-22 23:33:46 -05:00
a592318a6f impl idlike for widgetview 2026-01-20 18:11:21 -05:00
796bc41752 didn't actually remove state on widget remove (mem leak) 2026-01-19 21:39:13 -05:00
7bafb04a34 widget state 2026-01-19 20:39:58 -05:00
10 changed files with 174 additions and 218 deletions

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId, ActiveData, Event, EventCtx, EventFn, EventIdCtx, EventLike, HasEvents, IdLike, LayerId,
WeakWidget, Widget, WidgetEventFn, WidgetId, WeakWidget, WidgetEventFn, WidgetId,
util::{HashMap, HashSet, TypeMap}, util::{HashMap, HashSet, TypeMap},
}; };
use std::{any::TypeId, rc::Rc}; use std::{any::TypeId, rc::Rc};
@@ -24,15 +24,16 @@ impl<Rsc: HasEvents + 'static> EventManager<Rsc> {
self.types.type_or_default() self.types.type_or_default()
} }
pub fn register<W: Widget + ?Sized, E: EventLike>( pub fn register<I: IdLike + 'static, E: EventLike>(
&mut self, &mut self,
id: WeakWidget<W>, id: I,
event: E, event: E,
f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, W>, f: impl for<'a> WidgetEventFn<Rsc, <E::Event as Event>::Data<'a>, I::Widget>,
) { ) {
let i = id.id();
self.get_type::<E>().register(id, event, f); self.get_type::<E>().register(id, event, f);
self.widget_to_types self.widget_to_types
.entry(id.id()) .entry(i)
.or_default() .or_default()
.insert(Self::type_key::<E>()); .insert(Self::type_key::<E>());
} }
@@ -112,11 +113,11 @@ impl<Rsc: HasEvents, E: Event> Default for TypeEventManager<Rsc, E> {
} }
impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> { impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
fn register<W: Widget + ?Sized>( fn register<I: IdLike + 'static>(
&mut self, &mut self,
widget: WeakWidget<W>, widget: I,
event: impl EventLike<Event = E>, event: impl EventLike<Event = E>,
f: impl for<'a> WidgetEventFn<Rsc, E::Data<'a>, W>, f: impl for<'a> WidgetEventFn<Rsc, E::Data<'a>, I::Widget>,
) { ) {
let event = event.into_event(); let event = event.into_event();
self.map.entry(widget.id()).or_default().push(( self.map.entry(widget.id()).or_default().push((
@@ -124,7 +125,7 @@ impl<Rsc: HasEvents + 'static, E: Event> TypeEventManager<Rsc, E> {
Rc::new(move |ctx, rsc| { Rc::new(move |ctx, rsc| {
f( f(
EventIdCtx { EventIdCtx {
widget, widget: WeakWidget::new(widget.id()),
state: ctx.state, state: ctx.state,
data: ctx.data, data: ctx.data,
}, },

View File

@@ -1,196 +0,0 @@
use crate::{
ActiveData, Axis, EventsLike, Painter, SizeCtx, StrongWidget, UiRegion, UiRenderState, UiVec2,
WidgetId, Widgets,
render::MaskIdx,
util::{HashSet, forget_ref},
};
/// state maintained between widgets during painting
pub struct Drawer<'a> {
pub(super) widgets: &'a mut Widgets,
pub(super) data: &'a mut PainterData,
pub(super) events: &'a mut dyn EventsLike,
pub(super) render: &'a mut UiRenderState,
root: Option<&'a StrongWidget>,
draw_started: HashSet<WidgetId>,
}
impl<'a> Drawer<'a> {
/// redraws a widget that's currently active (drawn)
pub fn redraw(&mut self, id: WidgetId) {
self.widgets.needs_redraw.remove(&id);
self.draw_started.remove(&id);
// check if parent depends on the desired size of this, if so then redraw it first
for axis in [Axis::X, Axis::Y] {
if let Some(&(outer, old)) = self.render.cache.size.axis_dyn(axis).get(&id)
&& let Some(current) = self.render.active.get(&id)
&& let Some(pid) = current.parent
{
self.render.cache.size.axis_dyn(axis).remove(&id);
let new = self.size_ctx(id, outer).len_axis(id, axis);
self.render
.cache
.size
.axis_dyn(axis)
.insert(id, (outer, new));
if new != old {
self.redraw(pid);
}
}
}
if self.draw_started.contains(&id) {
return;
}
let Some(active) = self.remove(id, false) else {
return;
};
self.draw_inner(
active.layer,
id,
active.region,
active.parent,
active.mask,
Some(active.children),
);
}
pub(super) fn size_ctx<'b>(&'b mut self, source: WidgetId, outer: UiVec2) -> SizeCtx<'b> {
SizeCtx {
source,
cache: &mut self.render.cache,
text: &mut self.data.text,
textures: &mut self.data.textures,
widgets: &self.widgets,
outer,
output_size: self.render.output_size,
id: source,
}
}
pub(super) fn draw_inner(
&mut self,
layer: usize,
id: WidgetId,
region: UiRegion,
parent: Option<WidgetId>,
mask: MaskIdx,
old_children: Option<Vec<WidgetId>>,
) {
let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.render.active.get_mut(&id)
&& !self.widgets.needs_redraw.contains(&id)
{
// check to see if we can skip drawing first
if active.region == region {
return;
} else if active.region.size() == region.size() {
// TODO: epsilon?
let from = active.region;
self.mov(id, from, region);
return;
}
// if not, then maintain resize and track old children to remove unneeded
let active = self.remove(id, false).unwrap();
old_children = active.children;
}
// draw widget
self.draw_started.insert(id);
let mut painter = Painter {
drawer: self,
region,
mask,
layer,
id,
textures: Vec::new(),
primitives: Vec::new(),
children: Vec::new(),
};
let mut widget = painter.drawer.widgets.get_dyn_dynamic(id);
widget.draw(&mut painter);
drop(widget);
let Painter {
drawer: _,
region,
mask,
textures,
primitives,
children,
layer,
id,
} = painter;
// add to active
let active = ActiveData {
id,
region,
parent,
textures,
primitives,
children,
mask,
layer,
};
// remove old children that weren't kept
for c in &old_children {
if !active.children.contains(c) {
self.remove_rec(*c);
}
}
// update modules
self.events.draw(&active);
self.render.active.insert(id, active);
}
fn mov(&mut self, id: WidgetId, from: UiRegion, to: UiRegion) {
let active = self.render.active.get_mut(&id).unwrap();
for h in &active.primitives {
let region = self.render.layers[h.layer].region_mut(h);
*region = region.outside(&from).within(&to);
}
active.region = active.region.outside(&from).within(&to);
// SAFETY: children cannot be recursive
let children = unsafe { forget_ref(&active.children) };
for child in children {
self.mov(*child, from, to);
}
}
/// NOTE: instance textures are cleared and self.textures freed
fn remove(&mut self, id: WidgetId, undraw: bool) -> Option<ActiveData> {
let mut active = self.render.active.remove(&id);
if let Some(active) = &mut active {
for h in &active.primitives {
let mask = self.render.layers.free(h);
if mask != MaskIdx::NONE {
self.data.masks.remove(mask);
}
}
active.textures.clear();
self.data.textures.free();
if undraw {
self.events.undraw(active);
}
}
active
}
fn remove_rec(&mut self, id: WidgetId) -> Option<ActiveData> {
self.render.cache.remove(id);
let inst = self.remove(id, true);
if let Some(inst) = &inst {
for c in &inst.children {
self.remove_rec(*c);
}
}
inst
}
}

View File

@@ -2,11 +2,9 @@ use crate::{Mask, TextData, Textures, WeakWidget, WidgetId, Widgets, util::Track
mod active; mod active;
mod cache; mod cache;
// mod draw_state;
mod painter; mod painter;
mod render_state; mod render_state;
mod size; mod size;
mod state;
pub use active::*; pub use active::*;
pub use painter::Painter; pub use painter::Painter;

View File

@@ -1,3 +0,0 @@
// pub struct DynState {
//
// }

View File

@@ -1,6 +1,6 @@
use std::marker::Unsize; use std::marker::Unsize;
use crate::{Widget, WeakWidget}; use crate::{IdLike, WeakWidget, Widget};
pub trait WidgetView { pub trait WidgetView {
type Widget: Widget + ?Sized + Unsize<dyn Widget>; type Widget: Widget + ?Sized + Unsize<dyn Widget>;
@@ -14,3 +14,11 @@ pub trait HasWidget {
impl<W: Widget + Unsize<dyn Widget> + ?Sized> HasWidget for WeakWidget<W> { impl<W: Widget + Unsize<dyn Widget> + ?Sized> HasWidget for WeakWidget<W> {
type Widget = W; type Widget = W;
} }
impl<WV: WidgetView> IdLike for WV {
type Widget = WV::Widget;
fn id(&self) -> super::WidgetId {
self.root().id
}
}

View File

@@ -15,19 +15,22 @@ type Rsc = DefaultRsc<State>;
struct Test { struct Test {
#[root] #[root]
root: WeakWidget<Rect>, root: WeakWidget<Rect>,
cur: WeakState<bool>,
} }
impl Test { impl Test {
pub fn new(rsc: &mut Rsc) -> Self { pub fn new(rsc: &mut Rsc) -> Self {
let root = rect(Color::RED).add(rsc); let root = rect(Color::RED).add(rsc);
Self { root } let cur = rsc.create_state(root, false);
Self { root, cur }
} }
pub fn toggle(&self, rsc: &mut Rsc) { pub fn toggle(&self, rsc: &mut Rsc) {
let rect = (self.root)(rsc); let cur = &mut rsc[self.cur];
if rect.color == Color::RED { *cur = !*cur;
rect.color = Color::BLUE; if *cur {
rsc[self.root].color = Color::BLUE;
} else { } else {
rect.color = Color::RED; rsc[self.root].color = Color::RED;
} }
} }
} }

View File

@@ -4,12 +4,12 @@ My experimental attempt at a rust ui library (also my first ui library).
It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not. It's currently designed around using retained data structures (widgets), rather than diffing generated trees from data like xilem or iced. This is an experiment and I'm not sure if it's a good idea or not.
There's a `main.rs` that runs a testing window, so you can just `cargo run` to see it working. Examples are in `examples`, eg. `cargo run --example tabs`.
Goals, in general order: Goals, in general order:
1. does what I want it to (text, images, video, animations) 1. does what I want it to (text, images, video, animations)
2. very easy to use ignoring ergonomic ref counting 2. very easy to use ignoring ergonomic ref counting
3. reasonably fast / efficient (a lot faster than electron, save battery life) 3. reasonably fast / efficient (a lot faster than electron, save battery life, try to beat iced and xilem)
## dev details ## dev details

View File

@@ -17,6 +17,7 @@ mod event;
mod input; mod input;
mod render; mod render;
mod sense; mod sense;
mod state;
mod task; mod task;
pub use app::*; pub use app::*;
@@ -25,6 +26,7 @@ pub use event::*;
pub use input::*; pub use input::*;
pub use render::*; pub use render::*;
pub use sense::*; pub use sense::*;
pub use state::*;
pub use task::*; pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>; pub type Proxy<Event> = EventLoopProxy<Event>;
@@ -98,6 +100,7 @@ pub struct DefaultRsc<State: 'static> {
pub ui: UiData, pub ui: UiData,
pub events: EventManager<Self>, pub events: EventManager<Self>,
pub tasks: Tasks<Self>, pub tasks: Tasks<Self>,
pub state: WidgetState,
_state: PhantomData<State>, _state: PhantomData<State>,
} }
@@ -109,11 +112,16 @@ impl<State> DefaultRsc<State> {
ui: Default::default(), ui: Default::default(),
events: Default::default(), events: Default::default(),
tasks, tasks,
state: Default::default(),
_state: Default::default(), _state: Default::default(),
}, },
recv, recv,
) )
} }
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
self.state.add(id.id(), data)
}
} }
impl<State> UiRsc for DefaultRsc<State> { impl<State> UiRsc for DefaultRsc<State> {
@@ -135,6 +143,7 @@ impl<State> UiRsc for DefaultRsc<State> {
fn on_remove(&mut self, id: WidgetId) { fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id); self.events.remove(id);
self.state.remove(id);
} }
} }
@@ -158,6 +167,16 @@ impl<State: 'static> HasTasks for DefaultRsc<State> {
} }
} }
impl<State: 'static> HasWidgetState for DefaultRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
fn widget_state_mut(&mut self) -> &mut WidgetState {
&mut self.state
}
}
pub struct DefaultApp<State: DefaultAppState> { pub struct DefaultApp<State: DefaultAppState> {
rsc: DefaultRsc<State>, rsc: DefaultRsc<State>,
render: UiRenderState, render: UiRenderState,
@@ -289,3 +308,47 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
self.state.exit(&mut self.rsc, &mut self.render); self.state.exit(&mut self.rsc, &mut self.render);
} }
} }
pub trait RscIdx<Rsc> {
type Output;
fn get(self, rsc: &Rsc) -> &Self::Output;
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
}
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
index.get(self)
}
}
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> {
fn index_mut(&mut self, index: I) -> &mut Self::Output {
index.get_mut(self)
}
}
impl<W: Widget, Rsc: UiRsc> RscIdx<Rsc> for WeakWidget<W> {
type Output = W;
fn get(self, rsc: &Rsc) -> &Self::Output {
&rsc.ui().widgets[self]
}
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
&mut rsc.ui_mut().widgets[self]
}
}
impl<T: 'static, Rsc: HasWidgetState> RscIdx<Rsc> for WeakState<T> {
type Output = T;
fn get(self, rsc: &Rsc) -> &Self::Output {
rsc.widget_state().get(self)
}
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
rsc.widget_state_mut().get_mut(self)
}
}

75
src/default/state.rs Normal file
View File

@@ -0,0 +1,75 @@
use iris_core::{
WidgetId,
util::{HashMap, HashSet},
};
use std::{
any::{Any, TypeId},
marker::PhantomData,
};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Key {
id: WidgetId,
ty: TypeId,
i: usize,
}
#[derive(Default)]
pub struct WidgetState {
widgets: HashMap<WidgetId, HashSet<(TypeId, usize)>>,
counts: HashMap<(WidgetId, TypeId), usize>,
map: HashMap<Key, Box<dyn Any>>,
}
impl WidgetState {
pub fn new() -> Self {
Self::default()
}
pub fn add<T: 'static>(&mut self, id: WidgetId, data: T) -> WeakState<T> {
let ty = TypeId::of::<T>();
let count = self.counts.entry((id, ty)).or_default();
let i = *count;
let key = Key { ty, i, id };
self.map.insert(key, Box::new(data));
self.widgets.entry(id).or_default().insert((ty, i));
*count += 1;
WeakState {
key,
_pd: PhantomData,
}
}
pub fn remove(&mut self, id: WidgetId) {
for &(ty, i) in self.widgets.get(&id).into_iter().flatten() {
self.map.remove(&Key { id, ty, i });
}
}
pub fn get<T: 'static>(&self, state: WeakState<T>) -> &T {
self.map.get(&state.key).unwrap().downcast_ref().unwrap()
}
pub fn get_mut<T: 'static>(&mut self, state: WeakState<T>) -> &mut T {
self.map
.get_mut(&state.key)
.unwrap()
.downcast_mut()
.unwrap()
}
}
#[derive(Clone, Copy)]
pub struct WeakState<T> {
key: Key,
_pd: PhantomData<T>,
}
pub trait HasWidgetState {
fn widget_state(&self) -> &WidgetState;
fn widget_state_mut(&mut self) -> &mut WidgetState;
}
impl<'a, T: 'static> FnOnce<(&'a mut WidgetState,)> for WeakState<T> {
type Output = &'a mut T;
extern "rust-call" fn call_once(self, (state,): (&'a mut WidgetState,)) -> Self::Output {
state.get_mut(self)
}
}

View File

@@ -57,6 +57,13 @@ widget_trait! {
pub trait HasTasks: Sized + HasState + HasEvents { pub trait HasTasks: Sized + HasState + HasEvents {
fn tasks_mut(&mut self) -> &mut Tasks<Self>; fn tasks_mut(&mut self) -> &mut Tasks<Self>;
fn spawn_task<F: AsyncFnOnce(TaskCtx<Self>) + 'static + std::marker::Send>(&mut self, task: F)
where
F::CallOnceFuture: Send,
{
self.tasks_mut().spawn(task);
}
} }
pub trait AsyncWidgetEventFn<Rsc: HasEvents, W: ?Sized>: pub trait AsyncWidgetEventFn<Rsc: HasEvents, W: ?Sized>: