Compare commits

1 Commits

Author SHA1 Message Date
62aa02847a redo event fn signature & add event_ctx macro 2025-12-06 20:48:10 -05:00
10 changed files with 270 additions and 274 deletions

View File

@@ -1,20 +1,14 @@
use iris::{prelude::*, winit::*}; use iris::{prelude::*, winit::*};
fn main() { fn main() {
UiApp::<State>::run(); DefaultApp::<State>::run();
} }
struct State { struct State;
ui: DefaultUi,
}
impl DefaultUiState for State { impl DefaultAppState for State {
fn new(mut ui: DefaultUi, _proxy: Proxy<Self::Event>) -> Self { fn new(ui: &mut Ui, _state: &UiState, _proxy: Proxy<Self::Event>) -> Self {
rect(Color::RED).set_root(&mut ui); rect(Color::RED).set_root(ui);
Self { ui } Self
}
fn ui(&mut self) -> &mut DefaultUi {
&mut self.ui
} }
} }

View File

@@ -17,7 +17,7 @@ general ideas trynna use rn / experiment with:
- retained mode - retained mode
- specifically designed around wgpu - specifically designed around wgpu
- postfix functions for most things to prevent unreadable indentation (going very well) - postfix functions for most things to prevent unreadable indentation (going very well)
- no macros in user code / actual LSP typechecking (variadic generics if you can hear me please save us) - almost no macros in user code & actual LSP typechecking (variadic generics if you can hear me please save us)
- relative anchor + absolute offset coord system (+ "rest" / leftover during widget layout) - relative anchor + absolute offset coord system (+ "rest" / leftover during widget layout)
- single threaded ui & pass context around to make non async usage straightforward (pretty unsure about this) - single threaded ui & pass context around to make non async usage straightforward (pretty unsure about this)
- widgets store outside of the actual rendering so they can be moved around and swapped easily (unsure about this but seems to work good for now) - widgets store outside of the actual rendering so they can be moved around and swapped easily (unsure about this but seems to work good for now)

View File

@@ -7,16 +7,17 @@ use len_fns::*;
use winit::event::WindowEvent; use winit::event::WindowEvent;
fn main() { fn main() {
UiApp::<Client>::run(); DefaultApp::<Client>::run();
} }
pub struct Client { pub struct Client {
ui: DefaultUi,
info: WidgetId<Text>, info: WidgetId<Text>,
} }
impl DefaultUiState for Client { event_ctx!(Client);
fn new(mut ui: DefaultUi, _proxy: Proxy<Self::Event>) -> Self {
impl DefaultAppState for Client {
fn new(ui: &mut Ui, _state: &UiState, _proxy: Proxy<Self::Event>) -> Self {
let rrect = rect(Color::WHITE).radius(20); let rrect = rect(Color::WHITE).radius(20);
let pad_test = ( let pad_test = (
rrect.color(Color::BLUE), rrect.color(Color::BLUE),
@@ -39,7 +40,7 @@ impl DefaultUiState for Client {
.width(rest(3)), .width(rest(3)),
) )
.span(Dir::RIGHT) .span(Dir::RIGHT)
.add(&mut ui); .add(ui);
let span_test = ( let span_test = (
rrect.color(Color::GREEN).width(100), rrect.color(Color::GREEN).width(100),
@@ -50,14 +51,14 @@ impl DefaultUiState for Client {
rrect.color(Color::RED).width(100), rrect.color(Color::RED).width(100),
) )
.span(Dir::LEFT) .span(Dir::LEFT)
.add(&mut ui); .add(ui);
let span_add = Span::empty(Dir::RIGHT).add(&mut ui); let span_add = Span::empty(Dir::RIGHT).add(ui);
let span_add_ = span_add.clone(); let span_add_ = span_add.clone();
let add_button = rect(Color::LIME) let add_button = rect(Color::LIME)
.radius(30) .radius(30)
.on(CursorSense::click(), move |ctx: &mut Client, _| { .on(CursorSense::click(), move |ctx| {
let child = ctx let child = ctx
.ui .ui
.add(image(include_bytes!("assets/sungals.png")).center()) .add(image(include_bytes!("assets/sungals.png")).center())
@@ -70,13 +71,13 @@ impl DefaultUiState for Client {
let span_add_ = span_add.clone(); let span_add_ = span_add.clone();
let del_button = rect(Color::RED) let del_button = rect(Color::RED)
.radius(30) .radius(30)
.on(CursorSense::click(), move |ctx: &mut Client, _| { .on(CursorSense::click(), move |ctx| {
ctx.ui[&span_add_].children.pop(); ctx.ui[&span_add_].children.pop();
}) })
.sized((150, 150)) .sized((150, 150))
.align(Align::BOT_LEFT); .align(Align::BOT_LEFT);
let span_add_test = (span_add, add_button, del_button).stack().add(&mut ui); let span_add_test = (span_add, add_button, del_button).stack().add(ui);
let btext = |content| wtext(content).size(30); let btext = |content| wtext(content).size(30);
@@ -99,29 +100,27 @@ impl DefaultUiState for Client {
wtext("pretty cool right?").size(50), wtext("pretty cool right?").size(50),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.add(&mut ui); .add(ui);
let texts = Span::empty(Dir::DOWN).gap(10).add(&mut ui); let texts = Span::empty(Dir::DOWN).gap(10).add(ui);
let msg_area = texts.clone().scroll().masked().background(rect(Color::SKY)); let msg_area = texts.clone().scroll().masked().background(rect(Color::SKY));
let add_text = wtext("add") let add_text = wtext("add")
.editable(false) .editable(false)
.text_align(Align::LEFT) .text_align(Align::LEFT)
.size(30) .size(30)
.attr::<Selectable>(()) .attr::<Selectable>(())
.id_on(Submit, move |id, client: &mut Client, _| { .on(Submit, move |ctx| {
let content = client.ui.text(id).take(); let content = ctx.ui.text(ctx.id).take();
let text = wtext(content) let text = wtext(content)
.editable(false) .editable(false)
.size(30) .size(30)
.text_align(Align::LEFT) .text_align(Align::LEFT)
.wrap(true) .wrap(true)
.attr::<Selectable>(()); .attr::<Selectable>(());
let msg_box = text let msg_box = text.background(rect(Color::WHITE.darker(0.5))).add(ctx.ui);
.background(rect(Color::WHITE.darker(0.5))) ctx.ui[&texts].children.push(msg_box.any());
.add(&mut client.ui);
client.ui[&texts].children.push(msg_box.any());
}) })
.add(&mut ui); .add(ui);
let text_edit_scroll = ( let text_edit_scroll = (
msg_area.height(rest(1)), msg_area.height(rest(1)),
( (
@@ -129,8 +128,8 @@ impl DefaultUiState for Client {
( (
add_text.clone().width(rest(1)), add_text.clone().width(rest(1)),
Rect::new(Color::GREEN) Rect::new(Color::GREEN)
.on(CursorSense::click(), move |client: &mut Client, _| { .on(CursorSense::click(), move |ctx| {
client.run_event(&add_text, Submit, ()); ctx.ui.run_event(ctx.state, &add_text, Submit, ());
}) })
.sized((40, 40)), .sized((40, 40)),
) )
@@ -143,25 +142,25 @@ impl DefaultUiState for Client {
.align(Align::BOT), .align(Align::BOT),
) )
.span(Dir::DOWN) .span(Dir::DOWN)
.add(&mut ui); .add(ui);
let main = pad_test.clone().pad(10).add(&mut ui); let main = pad_test.clone().pad(10).add(ui);
let switch_button = |color, to: WidgetId, label| { let switch_button = |color, to: WidgetId, label| {
let main_ = main.clone(); let main_ = main.clone();
let rect = rect(color) let rect = rect(color)
.id_on(CursorSense::click(), move |id, ui: &mut Ui, _| { .on(CursorSense::click(), move |ctx| {
ui[&main_.clone()].inner = to.clone(); ctx.ui[&main_.clone()].inner = to.clone();
ui[id].color = color.darker(0.3); ctx.ui[ctx.id].color = color.darker(0.3);
}) })
.edit_on( .on(
CursorSense::HoverStart | CursorSense::unclick(), CursorSense::HoverStart | CursorSense::unclick(),
move |r, _| { move |ctx| {
r.color = color.brighter(0.2); ctx.ui[ctx.id].color = color.brighter(0.2);
}, },
) )
.edit_on(CursorSense::HoverEnd, move |r, _| { .on(CursorSense::HoverEnd, move |ctx| {
r.color = color; ctx.ui[ctx.id].color = color;
}); });
(rect, wtext(label).size(30).text_align(Align::CENTER)).stack() (rect, wtext(label).size(30).text_align(Align::CENTER)).stack()
}; };
@@ -179,33 +178,28 @@ impl DefaultUiState for Client {
) )
.span(Dir::RIGHT); .span(Dir::RIGHT);
let info = wtext("").add(&mut ui); let info = wtext("").add(ui);
let info_sect = info.clone().pad(10).align(Align::RIGHT); let info_sect = info.clone().pad(10).align(Align::RIGHT);
((tabs.height(40), main).span(Dir::DOWN), info_sect) ((tabs.height(40), main).span(Dir::DOWN), info_sect)
.stack() .stack()
.set_root(&mut ui); .set_root(ui);
Self { ui, info } Self { info }
} }
fn ui(&mut self) -> &mut DefaultUi { fn window_event(&mut self, _: WindowEvent, ui: &mut Ui, state: &UiState) {
&mut self.ui
}
fn window_event(&mut self, _: WindowEvent) {
let new = format!( let new = format!(
"widgets: {}\nactive:{}\nviews: {}", "widgets: {}\nactive:{}\nviews: {}",
self.ui.num_widgets(), ui.num_widgets(),
self.ui.active_widgets(), ui.active_widgets(),
self.ui.renderer.ui.view_count() state.renderer.ui.view_count()
); );
if new != *self.ui[&self.info].content { if new != *ui[&self.info].content {
*self.ui[&self.info].content = new; *ui[&self.info].content = new;
}
if ui.needs_redraw() {
state.window.request_redraw();
} }
if self.ui.needs_redraw() {
self.ui.window.request_redraw();
}
self.ui.input.end_frame();
} }
} }

View File

@@ -166,20 +166,21 @@ impl<Ctx> CursorModule<Ctx> {
} }
} }
pub trait SensorCtx: UiCtx { impl Ui {
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2); pub fn run_sensors<Ctx: 'static>(
} &mut self,
ctx: &mut Ctx,
impl<Ctx: UiCtx + 'static> SensorCtx for Ctx { cursor: &CursorState,
fn run_sensors(&mut self, cursor: &CursorState, window_size: Vec2) { window_size: Vec2,
CursorModule::<Ctx>::run(self, cursor, window_size); ) {
CursorModule::<Ctx>::run(self, ctx, cursor, window_size);
} }
} }
impl<Ctx: UiCtx + 'static> CursorModule<Ctx> { impl<Ctx: 'static> CursorModule<Ctx> {
pub fn run(ctx: &mut Ctx, cursor: &CursorState, window_size: Vec2) { pub fn run(ui: &mut Ui, ctx: &mut Ctx, cursor: &CursorState, window_size: Vec2) {
let layers = std::mem::take(&mut ctx.ui().data.layers); let layers = std::mem::take(&mut ui.data.layers);
let mut module = std::mem::take(ctx.ui().data.modules.get_mut::<Self>()); let mut module = std::mem::take(ui.data.modules.get_mut::<Self>());
for i in layers.indices().rev() { for i in layers.indices().rev() {
let Some(list) = module.active.get_mut(&i) else { let Some(list) = module.active.get_mut(&i) else {
@@ -204,7 +205,7 @@ impl<Ctx: UiCtx + 'static> CursorModule<Ctx> {
scroll_delta: cursor.scroll_delta, scroll_delta: cursor.scroll_delta,
sense, sense,
}; };
(sensor.f)(ctx, data); (sensor.f)(EventCtx { ui, state: ctx, data });
} }
} }
} }
@@ -213,10 +214,10 @@ impl<Ctx: UiCtx + 'static> CursorModule<Ctx> {
} }
} }
let ui_mod = ctx.ui().data.modules.get_mut::<Self>(); let ui_mod = ui.data.modules.get_mut::<Self>();
std::mem::swap(ui_mod, &mut module); std::mem::swap(ui_mod, &mut module);
ui_mod.merge(module); ui_mod.merge(module);
ctx.ui().data.layers = layers; ui.data.layers = layers;
} }
} }
@@ -305,7 +306,11 @@ impl<E: Event<Data = <CursorSenses as Event>::Data> + Into<CursorSenses>, Ctx: '
}); });
} }
fn run<'a>(&self, id: &Id, event: E) -> Option<impl Fn(&mut Ctx, E::Data) + use<'a, E, Ctx>> { fn run<'a>(
&self,
id: &Id,
event: E,
) -> Option<impl Fn(EventCtx<Ctx, <E as Event>::Data>) + use<'a, E, Ctx>> {
let senses = event.into(); let senses = event.into();
if let Some(group) = self.map.get(id) { if let Some(group) = self.map.get(id) {
let fs: Vec<_> = group let fs: Vec<_> = group
@@ -319,9 +324,13 @@ impl<E: Event<Data = <CursorSenses as Event>::Data> + Into<CursorSenses>, Ctx: '
} }
}) })
.collect(); .collect();
Some(move |ctx: &mut Ctx, data: CursorData| { Some(move |ctx: EventCtx<Ctx, CursorData>| {
for f in &fs { for f in &fs {
f(ctx, data.clone()); f(EventCtx {
state: ctx.state,
ui: ctx.ui,
data: ctx.data.clone(),
});
} }
}) })
} else { } else {

View File

@@ -100,10 +100,13 @@ impl<W: WidgetLike<Tag>, Tag> CoreWidget<W::Widget, Tag> for W {
} }
fn scroll(self) -> impl WidgetIdFn<Scroll> { fn scroll(self) -> impl WidgetIdFn<Scroll> {
event_ctx!(());
move |ui| { move |ui| {
Scroll::new(self.add(ui).any(), Axis::Y) Scroll::new(self.add(ui).any(), Axis::Y)
.edit_on(CursorSense::Scroll, |w, data| { .on(CursorSense::Scroll, |ctx| {
w.scroll(data.scroll_delta.y * 50.0); let s = &mut ctx.ui[ctx.id];
s.scroll(ctx.data.scroll_delta.y * 50.0);
}) })
.add(ui) .add(ui)
} }

View File

@@ -1,90 +1,97 @@
use std::{hash::Hash, rc::Rc}; use std::{hash::Hash, rc::Rc};
use crate::{ use crate::{
layout::{IdFnTag, Ui, UiModule, Widget, WidgetId, WidgetIdFn, WidgetLike}, layout::{IdFnTag, Ui, UiModule, WidgetId, WidgetIdFn, WidgetLike},
util::{HashMap, Id}, util::{HashMap, Id},
}; };
pub trait UiCtx {
fn ui(&mut self) -> &mut Ui;
}
impl UiCtx for Ui {
fn ui(&mut self) -> &mut Ui {
self
}
}
pub trait Event: Sized { pub trait Event: Sized {
type Module<Ctx: 'static>: EventModule<Self, Ctx>; type Module<Ctx: 'static>: EventModule<Self, Ctx>;
type Data: Clone; type Data: Clone;
} }
pub trait EventFn<Ctx, Data>: Fn(&mut Ctx, Data) + 'static {} pub struct EventCtx<'a, Ctx, Data> {
impl<F: Fn(&mut Ctx, Data) + 'static, Ctx, Data> EventFn<Ctx, Data> for F {} pub ui: &'a mut Ui,
pub state: &'a mut Ctx,
pub data: Data,
}
pub type ECtx<'a, Ctx, Data, W> = EventIdCtx<'a, Ctx, Data, W>;
pub struct EventIdCtx<'a, Ctx, Data, W> {
pub id: &'a WidgetId<W>,
pub ui: &'a mut Ui,
pub state: &'a mut Ctx,
pub data: Data,
}
pub trait EventFn<Ctx, Data>: Fn(EventCtx<Ctx, Data>) + 'static {}
impl<F: Fn(EventCtx<Ctx, Data>) + 'static, Ctx, Data> EventFn<Ctx, Data> for F {}
pub trait WidgetEventFn<Ctx, Data, W>: Fn(EventIdCtx<Ctx, Data, W>) + 'static {}
impl<F: Fn(EventIdCtx<Ctx, Data, W>) + 'static, Ctx, Data, W> WidgetEventFn<Ctx, Data, W> for F {}
#[macro_export]
macro_rules! event_ctx {
($ty: ty) => {
mod local_event_trait {
use super::*;
#[allow(unused_imports)]
use $crate::prelude::*;
pub trait EventableCtx<W, Tag, Ctx: 'static> {
fn on<E: Event>(
self,
event: E,
f: impl WidgetEventFn<Ctx, E::Data, W>,
) -> impl WidgetIdFn<W> + EventableCtx<W, IdFnTag, Ctx>;
}
impl<WL: WidgetLike<Tag>, Tag> EventableCtx<WL::Widget, Tag, $ty> for WL {
fn on<E: Event>(
self,
event: E,
f: impl WidgetEventFn<$ty, E::Data, WL::Widget>,
) -> impl WidgetIdFn<WL::Widget> + EventableCtx<WL::Widget, IdFnTag, $ty> {
eventable::Eventable::on(self, event, f)
}
}
}
use local_event_trait::*;
};
}
pub use event_ctx;
pub mod eventable {
use super::*;
pub trait Eventable<W, Tag> { pub trait Eventable<W, Tag> {
fn on<E: Event, Ctx: 'static>( fn on<E: Event, Ctx: 'static>(
self, self,
event: E, event: E,
f: impl EventFn<Ctx, E::Data>, f: impl WidgetEventFn<Ctx, E::Data, W>,
) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>; ) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>;
fn id_on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl Fn(&WidgetId<W>, &mut Ctx, E::Data) + 'static,
) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>
where
W: Widget;
fn edit_on<E: Event>(
self,
event: E,
f: impl Fn(&mut W, E::Data) + 'static,
) -> impl WidgetIdFn<W> + Eventable<W, IdFnTag>
where
W: Widget;
} }
impl<W: WidgetLike<Tag>, Tag> Eventable<W::Widget, Tag> for W { impl<WL: WidgetLike<Tag>, Tag> Eventable<WL::Widget, Tag> for WL {
fn on<E: Event, Ctx: 'static>( fn on<E: Event, Ctx: 'static>(
self, self,
event: E, event: E,
f: impl EventFn<Ctx, E::Data>, f: impl WidgetEventFn<Ctx, E::Data, WL::Widget>,
) -> impl WidgetIdFn<W::Widget> { ) -> impl WidgetIdFn<WL::Widget> {
move |ui| { move |ui| {
let id = self.add(ui); let id = self.add(ui);
ui.register_event(&id, event, f); let id_ = id.weak();
ui.register_event(&id, event, move |ctx| {
f(EventIdCtx {
id: &id_.strong(),
state: ctx.state,
data: ctx.data,
ui: ctx.ui,
});
});
id id
} }
} }
fn id_on<E: Event, Ctx: 'static>(
self,
event: E,
f: impl Fn(&WidgetId<W::Widget>, &mut Ctx, E::Data) + 'static,
) -> impl WidgetIdFn<W::Widget>
where
W::Widget: Widget,
{
self.with_id(move |ui, id| {
// needed so that this widget can actually be dropped
let id2 = id.weak();
id.on(event, move |ctx, pos| f(&id2.strong(), ctx, pos))
.add(ui)
})
}
fn edit_on<E: Event>(
self,
event: E,
f: impl Fn(&mut W::Widget, E::Data) + 'static,
) -> impl WidgetIdFn<W::Widget>
where
W::Widget: Widget,
{
self.id_on(event, move |id, ui: &mut Ui, pos| f(&mut ui[id], pos))
} }
} }
@@ -103,7 +110,7 @@ pub trait EventModule<E: Event, Ctx>: UiModule + Default {
&self, &self,
id: &Id, id: &Id,
event: E, event: E,
) -> Option<impl Fn(&mut Ctx, E::Data) + use<'a, Self, E, Ctx>>; ) -> Option<impl Fn(EventCtx<Ctx, E::Data>) + use<'a, Self, E, Ctx>>;
} }
type EventFnMap<Ctx, Data> = HashMap<Id, Vec<Rc<dyn EventFn<Ctx, Data>>>>; type EventFnMap<Ctx, Data> = HashMap<Id, Vec<Rc<dyn EventFn<Ctx, Data>>>>;
@@ -132,14 +139,22 @@ impl<E: HashableEvent, Ctx: 'static> EventModule<E, Ctx> for DefaultEventModule<
.push(Rc::new(f)); .push(Rc::new(f));
} }
fn run<'a>(&self, id: &Id, event: E) -> Option<impl Fn(&mut Ctx, E::Data) + use<'a, E, Ctx>> { fn run<'a>(
&self,
id: &Id,
event: E,
) -> Option<impl Fn(EventCtx<Ctx, E::Data>) + use<'a, E, Ctx>> {
if let Some(map) = self.map.get(&event) if let Some(map) = self.map.get(&event)
&& let Some(fs) = map.get(id) && let Some(fs) = map.get(id)
{ {
let fs = fs.clone(); let fs = fs.clone();
Some(move |ctx: &mut Ctx, data: E::Data| { Some(move |ctx: EventCtx<Ctx, <E as Event>::Data>| {
for f in &fs { for f in &fs {
f(ctx, data.clone()) f(EventCtx {
ui: ctx.ui,
state: ctx.state,
data: ctx.data.clone(),
})
} }
}) })
} else { } else {
@@ -149,14 +164,18 @@ impl<E: HashableEvent, Ctx: 'static> EventModule<E, Ctx> for DefaultEventModule<
} }
impl<E: HashableEvent, Ctx: 'static> DefaultEventModule<E, Ctx> { impl<E: HashableEvent, Ctx: 'static> DefaultEventModule<E, Ctx> {
pub fn run_all(&self, ctx: &mut Ctx, event: E, data: E::Data) pub fn run_all(&self, event: E, ctx: EventCtx<Ctx, E::Data>)
where where
E::Data: Clone, E::Data: Clone,
{ {
if let Some(map) = self.map.get(&event) { if let Some(map) = self.map.get(&event) {
for fs in map.values() { for fs in map.values() {
for f in fs { for f in fs {
f(ctx, data.clone()) f(EventCtx {
ui: ctx.ui,
state: ctx.state,
data: ctx.data.clone(),
})
} }
} }
} }
@@ -172,30 +191,24 @@ impl<E: Event + 'static, Ctx: 'static> Default for DefaultEventModule<E, Ctx> {
} }
impl Ui { impl Ui {
pub fn run_event<E: Event, Ctx: UiCtx + 'static, W>( pub fn run_event<E: Event, Ctx: 'static, W>(
&mut self,
ctx: &mut Ctx, ctx: &mut Ctx,
id: &WidgetId<W>, id: &WidgetId<W>,
event: E, event: E,
data: E::Data, data: E::Data,
) { ) {
if let Some(f) = ctx if let Some(f) = self
.ui()
.data .data
.modules .modules
.get_mut::<E::Module<Ctx>>() .get_mut::<E::Module<Ctx>>()
.run(&id.id, event) .run(&id.id, event)
{ {
f(ctx, data); f(EventCtx {
ui: self,
state: ctx,
data,
});
} }
} }
} }
pub trait EventCtx: UiCtx {
fn run_event<E: Event + Clone, W>(&mut self, id: &WidgetId<W>, event: E, data: E::Data);
}
impl<Ctx: UiCtx + 'static> EventCtx for Ctx {
fn run_event<E: Event + Clone, W>(&mut self, id: &WidgetId<W>, event: E, data: E::Data) {
Ui::run_event(self, id, event.clone(), data.clone());
}
}

View File

@@ -5,7 +5,7 @@ use winit::{
window::WindowId, window::WindowId,
}; };
pub trait UiState { pub trait AppState {
type Event: 'static; type Event: 'static;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self; fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop); fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
@@ -13,22 +13,22 @@ pub trait UiState {
fn exit(&mut self); fn exit(&mut self);
} }
pub struct UiApp<State: UiState> { pub struct App<State: AppState> {
state: Option<State>, state: Option<State>,
proxy: EventLoopProxy<State::Event>, proxy: EventLoopProxy<State::Event>,
} }
impl<State: UiState> UiApp<State> { impl<State: AppState> App<State> {
pub fn run() { pub fn run() {
let event_loop = EventLoop::with_user_event().build().unwrap(); let event_loop = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy(); let proxy = event_loop.create_proxy();
event_loop event_loop
.run_app(&mut UiApp::<State> { state: None, proxy }) .run_app(&mut App::<State> { state: None, proxy })
.unwrap(); .unwrap();
} }
} }
impl<State: UiState> ApplicationHandler<State::Event> for UiApp<State> { impl<State: AppState> ApplicationHandler<State::Event> for App<State> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) { fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_none() { if self.state.is_none() {
let state = State::new(event_loop, self.proxy.clone()); let state = State::new(event_loop, self.proxy.clone());

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, winit::DefaultUi}; use crate::{prelude::*, winit::UiState};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use winit::dpi::{LogicalPosition, LogicalSize}; use winit::dpi::{LogicalPosition, LogicalSize};
@@ -12,13 +12,14 @@ impl<W: 'static> WidgetAttr<W> for Selector {
ui.register_event( ui.register_event(
&container.clone(), &container.clone(),
CursorSense::click_or_drag(), CursorSense::click_or_drag(),
move |ui: &mut DefaultUi, mut data| { move |mut ctx| {
let ui = ctx.ui;
let region = ui.window_region(&id).unwrap(); let region = ui.window_region(&id).unwrap();
let id_pos = region.top_left; let id_pos = region.top_left;
let container_pos = ui.window_region(&container).unwrap().top_left; let container_pos = ui.window_region(&container).unwrap().top_left;
data.cursor += container_pos - id_pos; ctx.data.cursor += container_pos - id_pos;
data.size = region.size(); ctx.data.size = region.size();
select(id.clone(), ui, data); select(ui, id.clone(), ctx.state, ctx.data);
}, },
); );
} }
@@ -31,29 +32,24 @@ impl WidgetAttr<TextEdit> for Selectable {
fn run(ui: &mut Ui, id: &WidgetId<TextEdit>, _: Self::Input) { fn run(ui: &mut Ui, id: &WidgetId<TextEdit>, _: Self::Input) {
let id = id.clone(); let id = id.clone();
ui.register_event( ui.register_event(&id.clone(), CursorSense::click_or_drag(), move |ctx| {
&id.clone(), select(ctx.ui, id.clone(), ctx.state, ctx.data);
CursorSense::click_or_drag(), });
move |ui: &mut DefaultUi, data| {
select(id.clone(), ui, data);
},
);
} }
} }
fn select(id: WidgetId<TextEdit>, ui: &mut DefaultUi, data: CursorData) { fn select(ui: &mut Ui, id: WidgetId<TextEdit>, state: &mut UiState, data: CursorData) {
let now = Instant::now(); let now = Instant::now();
let recent = (now - ui.last_click) < Duration::from_millis(300); let recent = (now - state.last_click) < Duration::from_millis(300);
ui.last_click = now; state.last_click = now;
ui.ui ui.text(&id)
.text(&id)
.select(data.cursor, data.size, data.sense.is_dragging(), recent); .select(data.cursor, data.size, data.sense.is_dragging(), recent);
if let Some(region) = ui.ui.window_region(&id) { if let Some(region) = ui.window_region(&id) {
ui.window.set_ime_allowed(true); state.window.set_ime_allowed(true);
ui.window.set_ime_cursor_area( state.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()), LogicalPosition::<f32>::from(region.top_left.tuple()),
LogicalSize::<f32>::from(region.size().tuple()), LogicalSize::<f32>::from(region.size().tuple()),
); );
} }
ui.focus = Some(id); state.focus = Some(id);
} }

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
core::{CursorState, Modifiers}, core::{CursorState, Modifiers},
layout::Vec2, layout::Vec2,
winit::DefaultUi, winit::UiState,
}; };
use winit::{ use winit::{
event::{MouseButton, MouseScrollDelta, WindowEvent}, event::{MouseButton, MouseScrollDelta, WindowEvent},
@@ -70,7 +70,7 @@ impl Input {
} }
} }
impl DefaultUi { impl UiState {
pub fn window_size(&self) -> Vec2 { pub fn window_size(&self) -> Vec2 {
let size = self.renderer.window().inner_size(); let size = self.renderer.window().inner_size();
(size.width, size.height).into() (size.width, size.height).into()

View File

@@ -2,7 +2,6 @@ use crate::prelude::*;
use crate::winit::event::{Edited, Submit}; use crate::winit::event::{Edited, Submit};
use crate::winit::{input::Input, render::UiRenderer}; use crate::winit::{input::Input, render::UiRenderer};
use arboard::Clipboard; use arboard::Clipboard;
use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use winit::event::{Ime, WindowEvent}; use winit::event::{Ime, WindowEvent};
@@ -18,11 +17,17 @@ pub mod render;
pub use app::*; pub use app::*;
pub type Proxy<Event> = EventLoopProxy<Event>; pub type Proxy<Event> = EventLoopProxy<Event>;
pub type DefaultApp<Data> = App<DefaultState<Data>>;
pub struct DefaultUi { pub struct DefaultState<AppState> {
ui: Ui,
ui_state: UiState,
app_state: AppState,
}
pub struct UiState {
pub renderer: UiRenderer, pub renderer: UiRenderer,
pub input: Input, pub input: Input,
pub ui: Ui,
pub focus: Option<WidgetId<TextEdit>>, pub focus: Option<WidgetId<TextEdit>>,
pub clipboard: Clipboard, pub clipboard: Clipboard,
pub window: Arc<Window>, pub window: Arc<Window>,
@@ -30,62 +35,71 @@ pub struct DefaultUi {
pub last_click: Instant, pub last_click: Instant,
} }
pub trait DefaultUiState: 'static { pub trait DefaultAppState: 'static {
type Event: 'static = (); type Event: 'static = ();
fn new(ui: DefaultUi, proxy: Proxy<Self::Event>) -> Self; fn new(ui: &mut Ui, state: &UiState, proxy: Proxy<Self::Event>) -> Self;
fn ui(&mut self) -> &mut DefaultUi;
#[allow(unused_variables)] #[allow(unused_variables)]
fn event(&mut self, event: Self::Event) {} fn event(&mut self, event: Self::Event, ui: &mut Ui, state: &UiState) {}
fn exit(&mut self) {}
#[allow(unused_variables)] #[allow(unused_variables)]
fn window_event(&mut self, event: WindowEvent) {} fn exit(&mut self, ui: &mut Ui, state: &UiState) {}
#[allow(unused_variables)]
fn window_event(&mut self, event: WindowEvent, ui: &mut Ui, state: &UiState) {}
fn window_attrs() -> WindowAttributes { fn window_attrs() -> WindowAttributes {
WindowAttributes::default() WindowAttributes::default()
} }
} }
impl<State: DefaultUiState> UiState for State { impl<State: DefaultAppState> AppState for DefaultState<State> {
type Event = State::Event; type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self { fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = Arc::new( let window = Arc::new(
event_loop event_loop
.create_window(Self::window_attrs()) .create_window(State::window_attrs())
.expect("failed to create window "), .expect("failed to create window "),
); );
let ui = DefaultUi { let mut ui = Ui::new();
let ui_state = UiState {
renderer: UiRenderer::new(window.clone()), renderer: UiRenderer::new(window.clone()),
window, window,
input: Input::default(), input: Input::default(),
ui: Ui::new(),
clipboard: Clipboard::new().unwrap(), clipboard: Clipboard::new().unwrap(),
ime: 0, ime: 0,
last_click: Instant::now(), last_click: Instant::now(),
focus: None, focus: None,
}; };
State::new(ui, proxy) let app_state = State::new(&mut ui, &ui_state, proxy);
Self {
ui,
ui_state,
app_state,
}
} }
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) { fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.event(event); self.app_state.event(event, &mut self.ui, &self.ui_state);
} }
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) { fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let ui = self.ui(); let Self {
let input_changed = ui.input.event(&event); ui,
let cursor_state = ui.cursor_state().clone(); ui_state: ui_data,
let old = ui.focus.clone(); app_state: app_data,
} = self;
let input_changed = ui_data.input.event(&event);
let cursor_state = ui_data.cursor_state().clone();
let old = ui_data.focus.clone();
if cursor_state.buttons.left.is_start() { if cursor_state.buttons.left.is_start() {
ui.focus = None; ui_data.focus = None;
} }
if input_changed { if input_changed {
let window_size = ui.window_size(); let window_size = ui_data.window_size();
ui.run_sensors(&cursor_state, window_size); ui.run_sensors(&mut (), &cursor_state, window_size);
ui.ui.run_sensors(&cursor_state, window_size); ui.run_sensors(ui_data, &cursor_state, window_size);
self.run_sensors(&cursor_state, window_size); ui.run_sensors(app_data, &cursor_state, window_size);
} }
let ui = self.ui(); if old != ui_data.focus
if old != ui.focus
&& let Some(old) = old && let Some(old) = old
{ {
ui.text(&old).deselect(); ui.text(&old).deselect();
@@ -93,55 +107,55 @@ impl<State: DefaultUiState> UiState for State {
match &event { match &event {
WindowEvent::CloseRequested => event_loop.exit(), WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
ui.ui.update(); ui.update();
ui.renderer.update(&mut ui.ui); ui_data.renderer.update(ui);
ui.renderer.draw(); ui_data.renderer.draw();
} }
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => {
ui.ui.resize((size.width, size.height)); ui.resize((size.width, size.height));
ui.renderer.resize(size) ui_data.renderer.resize(size)
} }
WindowEvent::KeyboardInput { event, .. } => { WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = &ui.focus if let Some(sel) = &ui_data.focus
&& event.state.is_pressed() && event.state.is_pressed()
{ {
let sel = &sel.clone(); let sel = &sel.clone();
let mut text = ui.ui.text(sel); let mut text = ui.text(sel);
match text.apply_event(event, &ui.input.modifiers) { match text.apply_event(event, &ui_data.input.modifiers) {
TextInputResult::Unfocus => { TextInputResult::Unfocus => {
ui.focus = None; ui_data.focus = None;
ui.window.set_ime_allowed(false); ui_data.window.set_ime_allowed(false);
} }
TextInputResult::Submit => { TextInputResult::Submit => {
self.run_event(sel, Submit, ()); ui.run_event(app_data, sel, Submit, ());
} }
TextInputResult::Paste => { TextInputResult::Paste => {
if let Ok(t) = ui.clipboard.get_text() { if let Ok(t) = ui_data.clipboard.get_text() {
text.insert(&t); text.insert(&t);
} }
self.run_event(sel, Edited, ()); ui.run_event(app_data, sel, Edited, ());
} }
TextInputResult::Copy(text) => { TextInputResult::Copy(text) => {
if let Err(err) = ui.clipboard.set_text(text) { if let Err(err) = ui_data.clipboard.set_text(text) {
eprintln!("failed to copy text to clipboard: {err}") eprintln!("failed to copy text to clipboard: {err}")
} }
} }
TextInputResult::Used => { TextInputResult::Used => {
self.run_event(sel, Edited, ()); ui.run_event(app_data, sel, Edited, ());
} }
TextInputResult::Unused => {} TextInputResult::Unused => {}
} }
} }
} }
WindowEvent::Ime(ime) => { WindowEvent::Ime(ime) => {
if let Some(sel) = &ui.focus { if let Some(sel) = &ui_data.focus {
let mut text = ui.ui.text(sel); let mut text = ui.text(sel);
match ime { match ime {
Ime::Enabled | Ime::Disabled => (), Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => { Ime::Preedit(content, _pos) => {
// TODO: highlight once that's real // TODO: highlight once that's real
text.replace(ui.ime, content); text.replace(ui_data.ime, content);
ui.ime = content.chars().count(); ui_data.ime = content.chars().count();
} }
Ime::Commit(content) => { Ime::Commit(content) => {
text.insert(content); text.insert(content);
@@ -151,41 +165,14 @@ impl<State: DefaultUiState> UiState for State {
} }
_ => (), _ => (),
} }
self.window_event(event); app_data.window_event(event, ui, ui_data);
let ui = self.ui();
if ui.needs_redraw() { if ui.needs_redraw() {
ui.renderer.window().request_redraw(); ui_data.renderer.window().request_redraw();
} }
ui.input.end_frame(); ui_data.input.end_frame();
} }
fn exit(&mut self) { fn exit(&mut self) {
self.exit(); self.app_state.exit(&mut self.ui, &self.ui_state);
}
}
impl<T: DefaultUiState> UiCtx for T {
fn ui(&mut self) -> &mut Ui {
&mut self.ui().ui
}
}
impl UiCtx for DefaultUi {
fn ui(&mut self) -> &mut Ui {
&mut self.ui
}
}
impl Deref for DefaultUi {
type Target = Ui;
fn deref(&self) -> &Self::Target {
&self.ui
}
}
impl DerefMut for DefaultUi {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ui
} }
} }