6 Commits

Author SHA1 Message Date
1102dc7338 work 2026-02-26 19:18:27 -05:00
1aadef0e7e fix Draw (redraw) 2026-02-21 00:19:39 -05:00
426ff0adfc oop 2026-02-18 16:49:59 -05:00
dab6cf298a Merge branch 'work' of git.arirex.me:shadowcat/iris into work 2026-02-17 18:14:38 -05:00
38d896d44d selector 2026-02-17 18:14:19 -05:00
7b54aaf3c4 readme 2026-01-29 16:39:19 -05:00
13 changed files with 223 additions and 78 deletions

View File

@@ -28,7 +28,7 @@ pub trait UiRsc {
#[allow(unused_variables)]
fn on_remove(&mut self, id: WidgetId) {}
#[allow(unused_variables)]
fn on_draw(&mut self, active: &ActiveData) {}
fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {}
#[allow(unused_variables)]
fn on_undraw(&mut self, active: &ActiveData) {}

View File

@@ -81,10 +81,12 @@ impl UiRenderState {
old_children: Option<Vec<WidgetId>>,
rsc: &mut dyn UiRsc,
) {
let mut redrawn = old_children.is_some();
let mut old_children = old_children.unwrap_or_default();
if let Some(active) = self.active.get_mut(&id)
&& !rsc.widgets().needs_redraw.contains(&id)
{
redrawn = true;
// check to see if we can skip drawing first
if active.region == region {
return;
@@ -149,7 +151,7 @@ impl UiRenderState {
}
}
rsc.on_draw(&active);
rsc.on_draw(&active, redrawn);
self.active.insert(id, active);
}

View File

@@ -10,11 +10,7 @@ struct State {
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
rect(Color::RED).set_root(rsc, &mut ui_state);
Self { ui_state }
}

View File

@@ -16,11 +16,7 @@ pub struct Client {
}
impl DefaultAppState for Client {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
let rrect = rect(Color::WHITE).radius(20);
let pad_test = (
rrect.color(Color::BLUE),
@@ -148,7 +144,7 @@ impl DefaultAppState for Client {
.span(Dir::DOWN)
.add(rsc);
let main = WidgetPtr::new().add(rsc);
let main = WidgetPtr::empty().add(rsc);
let vals = Rc::new(RefCell::new((0, Vec::new())));
let mut switch_button = |color, to: WeakWidget, label| {

View File

@@ -11,11 +11,7 @@ struct State {
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
let rect = rect(Color::RED).add(rsc);
rect.task_on(CursorSense::click(), async move |mut ctx| {
tokio::time::sleep(Duration::from_secs(1)).await;

View File

@@ -36,11 +36,7 @@ impl Test {
}
impl DefaultAppState for State {
fn new(
mut ui_state: DefaultUiState,
rsc: &mut DefaultRsc<Self>,
_: Proxy<Self::Event>,
) -> Self {
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
let test = Test::new(rsc);
test.on(CursorSense::click(), move |_, rsc| {

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.
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:
1. does what I want it to (text, images, video, animations)
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

View File

@@ -7,3 +7,11 @@ impl Event for Submit {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Edited;
impl Event for Edited {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Draw;
impl Event for Draw {}
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Undraw;
impl Event for Undraw {}

View File

@@ -29,7 +29,26 @@ pub use sense::*;
pub use state::*;
pub use task::*;
pub type Proxy<Event> = EventLoopProxy<Event>;
pub struct EventSender<State: DefaultAppState> {
proxy: EventLoopProxy<UiMainEvent<State>>,
}
impl<State: DefaultAppState> Clone for EventSender<State> {
fn clone(&self) -> Self {
Self {
proxy: self.proxy.clone(),
}
}
}
impl<State: DefaultAppState> EventSender<State> {
pub fn send(&self, event: State::Event) {
let _ = self.proxy.send_event(UiMainEvent::App(event));
}
pub fn run(&self, f: impl MainCallback<State>) {
let _ = self.proxy.send_event(UiMainEvent::Callback(Box::new(f)));
}
}
pub struct DefaultUiState {
pub root: Option<StrongWidget>,
@@ -70,9 +89,8 @@ pub trait HasDefaultUiState: Sized + 'static {
}
pub trait DefaultAppState: HasDefaultUiState {
type Event = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>, proxy: Proxy<Self::Event>)
-> Self;
type Event: Send = ();
fn new(ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self;
#[allow(unused_variables)]
fn event(
&mut self,
@@ -96,23 +114,54 @@ pub trait DefaultAppState: HasDefaultUiState {
}
}
pub struct DefaultRsc<State: 'static> {
pub struct DefaultRsc<State: 'static + DefaultAppState> {
pub ui: UiData,
pub events: EventManager<Self>,
pub tasks: Tasks<Self>,
pub state: WidgetState,
pub widget_events: Vec<WidgetEvent>,
pub window_event: EventSender<State>,
_state: PhantomData<State>,
}
impl<State> DefaultRsc<State> {
fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Self>) {
let (tasks, recv) = Tasks::init(window);
pub struct WidgetEvent {
id: WidgetId,
ty: WidgetEventType,
}
pub enum WidgetEventType {
Draw,
Undraw,
Remove,
}
pub trait MainCallback<State>: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static {}
impl<F: FnOnce(&mut DefaultRsc<State>) + Sync + Send + 'static, State> MainCallback<State> for F {}
pub enum UiMainEvent<State: DefaultAppState> {
RequestUpdate,
Callback(Box<dyn MainCallback<State>>),
App(State::Event),
}
impl<State: DefaultAppState> DefaultRsc<State> {
fn init(proxy: EventLoopProxy<UiMainEvent<State>>) -> (Self, TaskMsgReceiver<Self>) {
let window_event = EventSender {
proxy: proxy.clone(),
};
let (tasks, recv) = Tasks::init(move || {
if proxy.send_event(UiMainEvent::RequestUpdate).is_err() {
panic!("main thread blew up or smth");
}
});
(
Self {
ui: Default::default(),
events: Default::default(),
tasks,
widget_events: Default::default(),
state: Default::default(),
window_event,
_state: Default::default(),
},
recv,
@@ -124,7 +173,7 @@ impl<State> DefaultRsc<State> {
}
}
impl<State> UiRsc for DefaultRsc<State> {
impl<State: DefaultAppState> UiRsc for DefaultRsc<State> {
fn ui(&self) -> &UiData {
&self.ui
}
@@ -133,25 +182,39 @@ impl<State> UiRsc for DefaultRsc<State> {
&mut self.ui
}
fn on_draw(&mut self, active: &ActiveData) {
fn on_draw(&mut self, active: &ActiveData, redrawn: bool) {
self.events.draw(active);
if !redrawn {
self.widget_events.push(WidgetEvent {
id: active.id,
ty: WidgetEventType::Draw,
});
}
}
fn on_undraw(&mut self, active: &ActiveData) {
self.events.undraw(active);
self.widget_events.push(WidgetEvent {
id: active.id,
ty: WidgetEventType::Undraw,
});
}
fn on_remove(&mut self, id: WidgetId) {
self.events.remove(id);
self.state.remove(id);
self.widget_events.push(WidgetEvent {
id,
ty: WidgetEventType::Remove,
});
}
}
impl<State: 'static> HasState for DefaultRsc<State> {
impl<State: 'static + DefaultAppState> HasState for DefaultRsc<State> {
type State = State;
}
impl<State: 'static> HasEvents for DefaultRsc<State> {
impl<State: 'static + DefaultAppState> HasEvents for DefaultRsc<State> {
fn events(&self) -> &EventManager<Self> {
&self.events
}
@@ -161,13 +224,13 @@ impl<State: 'static> HasEvents for DefaultRsc<State> {
}
}
impl<State: 'static> HasTasks for DefaultRsc<State> {
impl<State: 'static + DefaultAppState> HasTasks for DefaultRsc<State> {
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
&mut self.tasks
}
}
impl<State: 'static> HasWidgetState for DefaultRsc<State> {
impl<State: 'static + DefaultAppState> HasWidgetState for DefaultRsc<State> {
fn widget_state(&self) -> &WidgetState {
&self.state
}
@@ -185,15 +248,15 @@ pub struct DefaultApp<State: DefaultAppState> {
}
impl<State: DefaultAppState> AppState for DefaultApp<State> {
type Event = State::Event;
type Event = UiMainEvent<State>;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = event_loop
.create_window(State::window_attributes())
.unwrap();
let default_state = DefaultUiState::new(window);
let (mut rsc, task_recv) = DefaultRsc::init(default_state.window.clone());
let state = State::new(default_state, &mut rsc, proxy);
let (mut rsc, task_recv) = DefaultRsc::init(proxy);
let state = State::new(default_state, &mut rsc);
let render = UiRenderState::new();
Self {
rsc,
@@ -204,38 +267,39 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
match event {
UiMainEvent::RequestUpdate => {
self.check_updates();
}
UiMainEvent::App(event) => {
self.state.event(event, &mut self.rsc, &mut self.render);
}
UiMainEvent::Callback(f) => f(&mut self.rsc),
}
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let Self {
rsc,
render,
state,
task_recv,
rsc, render, state, ..
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
// input handling
let ui_state = state.default_state_mut();
let input_changed = ui_state.input.event(&event);
if ui_state.input.event(&event) {
let cursor_state = ui_state.cursor_state().clone();
let old = ui_state.focus;
if cursor_state.buttons.left.is_start() {
ui_state.focus = None;
}
if input_changed {
let window_size = ui_state.window_size();
render.run_sensors(rsc, state, cursor_state, window_size);
}
let ui_state = state.default_state_mut();
if old != ui_state.focus
if old != state.default_state().focus
&& let Some(old) = old
{
old.edit(rsc).deselect();
}
}
let ui_state = state.default_state_mut();
match &event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
@@ -297,11 +361,9 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
_ => (),
}
state.window_event(event, rsc, render);
let ui_state = self.state.default_state_mut();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
ui_state.input.end_frame();
self.check_updates();
self.state.default_state_mut().input.end_frame();
}
fn exit(&mut self) {
@@ -309,13 +371,49 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
}
}
impl<State: DefaultAppState> DefaultApp<State> {
pub fn check_updates(&mut self) {
let Self {
rsc,
render,
state,
task_recv,
} = self;
for update in task_recv.try_iter() {
update(state, rsc);
}
let mut events = std::mem::take(&mut rsc.widget_events);
for event in events.drain(..) {
match event.ty {
WidgetEventType::Draw => {
rsc.run_event::<Draw>(event.id, (), state);
}
WidgetEventType::Undraw => {
rsc.run_event::<Undraw>(event.id, (), state);
}
_ => (),
}
}
rsc.widget_events = events;
let ui_state = state.default_state();
if render.needs_redraw(&ui_state.root, rsc.widgets()) {
ui_state.renderer.window().request_redraw();
}
}
}
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> {
impl<State: 'static + DefaultAppState, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I>
for DefaultRsc<State>
{
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
@@ -323,7 +421,9 @@ impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for Defaul
}
}
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for DefaultRsc<State> {
impl<State: 'static + DefaultAppState, 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)
}

View File

@@ -13,7 +13,6 @@ use tokio::{
unbounded_channel as async_channel,
},
};
use winit::window::Window;
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
@@ -23,7 +22,7 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
pub struct Tasks<Rsc: HasState> {
start: AsyncSender<BoxTask>,
window: Arc<Window>,
request_update: Arc<dyn Fn() + Send + Sync>,
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
}
@@ -45,7 +44,7 @@ impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
impl<Rsc: HasState> Tasks<Rsc> {
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
pub fn init(request_update: impl Fn() + 'static + Send + Sync) -> (Self, TaskMsgReceiver<Rsc>) {
let (start, start_recv) = async_channel();
let (msgs, msgs_recv) = sync_channel();
std::thread::spawn(|| {
@@ -56,7 +55,7 @@ impl<Rsc: HasState> Tasks<Rsc> {
Self {
start,
msg_send: msgs,
window,
request_update: Arc::new(request_update),
},
msgs_recv,
)
@@ -67,10 +66,10 @@ impl<Rsc: HasState> Tasks<Rsc> {
F::CallOnceFuture: Send,
{
let send = self.msg_send.clone();
let window = self.window.clone();
let request_update = self.request_update.clone();
let _ = self.start.send(Box::pin(async move {
task(TaskCtx::new(send)).await;
window.request_redraw();
request_update();
}));
}
}

View File

@@ -5,6 +5,7 @@ mod ptr;
mod rect;
mod text;
mod trait_fns;
mod selector;
pub use image::*;
pub use mask::*;
@@ -13,3 +14,4 @@ pub use ptr::*;
pub use rect::*;
pub use text::*;
pub use trait_fns::*;
pub use selector::*;

View File

@@ -30,8 +30,10 @@ impl Widget for WidgetPtr {
}
impl WidgetPtr {
pub fn new() -> Self {
Self::default()
pub fn new(widget: StrongWidget) -> Self {
Self {
inner: Some(widget),
}
}
pub fn empty() -> Self {
Self {

48
src/widget/selector.rs Normal file
View File

@@ -0,0 +1,48 @@
use std::hash::Hash;
use iris_core::util::HashMap;
use crate::prelude::*;
pub struct WidgetSelector<T> {
current: (T, StrongWidget),
map: HashMap<T, StrongWidget>,
}
impl<T: Hash + Eq> WidgetSelector<T> {
pub fn new(key: T, widget: StrongWidget) -> Self {
Self {
current: (key, widget),
map: Default::default(),
}
}
pub fn set(&mut self, key: T, widget: StrongWidget) {
self.map.insert(key, widget);
}
pub fn select(&mut self, key: T) -> bool {
if let Some(val) = self.map.remove(&key) {
let mut new = (key, val);
std::mem::swap(&mut new, &mut self.current);
self.map.insert(new.0, new.1);
true
} else {
false
}
}
}
impl<T: 'static> Widget for WidgetSelector<T> {
fn draw(&mut self, painter: &mut Painter) {
painter.widget(&self.current.1);
}
fn desired_width(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.width(&self.current.1)
}
fn desired_height(&mut self, ctx: &mut SizeCtx) -> Len {
ctx.height(&self.current.1)
}
}