Compare commits

2 Commits

Author SHA1 Message Date
f7b100e00c add default winit framework 2025-12-04 14:31:07 -05:00
e5d0a7e592 fix on_id widget refcount leak (-> memory leak) 2025-12-04 02:59:05 -05:00
16 changed files with 431 additions and 347 deletions

View File

@@ -7,7 +7,7 @@ it's called iris because it's the structure around what you actually want to dis
there's a `main.rs` that runs a testing window, so can just `cargo run` to see it working
goals, in general order:
1. does what I want it to (video, text, animations)
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)
@@ -22,5 +22,5 @@ general ideas trynna use rn / experiment with:
- 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)
under heavy initial development so not gonna try to explain status, check TODO for that maybe
under heavy initial development so not gonna try to explain status, maybe check TODO for that;
sizable chance it gets a rewrite once I know everything I need and what seems to work best

View File

@@ -1,37 +0,0 @@
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Window, WindowId},
};
use super::Client;
#[derive(Default)]
pub struct App {
client: Option<Client>,
}
impl App {
pub fn run() {
let event_loop = EventLoop::new().unwrap();
event_loop.run_app(&mut App::default()).unwrap();
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.client.is_none() {
let window = event_loop
.create_window(Window::default_attributes())
.unwrap();
let client = Client::new(window.into());
self.client = Some(client);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let client = self.client.as_mut().unwrap();
client.event(event, event_loop);
}
}

View File

@@ -1,86 +1,24 @@
use app::App;
use arboard::Clipboard;
use cosmic_text::Family;
use input::Input;
use iris::prelude::*;
use iris::{
prelude::*,
winit::{attr::Selectable, event::Submit, *},
};
use len_fns::*;
use render::Renderer;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use winit::{
dpi::{LogicalPosition, LogicalSize},
event::{Ime, WindowEvent},
event_loop::ActiveEventLoop,
window::Window,
};
mod app;
mod input;
mod render;
use winit::event::WindowEvent;
fn main() {
App::run();
UiApp::<Client>::run();
}
pub struct Client {
renderer: Renderer,
window: Arc<Window>,
input: Input,
ui: Ui,
ui: DefaultUi,
info: WidgetId<Text>,
focus: Option<WidgetId<TextEdit>>,
clipboard: Clipboard,
ime: usize,
last_click: Instant,
}
#[derive(Eq, PartialEq, Hash, Clone)]
struct Submit;
impl DefaultUiState for Client {
type Event = ();
impl DefaultEvent for Submit {
type Data = ();
}
pub struct Selectable;
impl WidgetAttr<TextEdit> for Selectable {
type Input = ();
fn run(ui: &mut Ui, id: &WidgetId<TextEdit>, _: Self::Input) {
let id = id.clone();
ui.register_event(
&id.clone(),
CursorSense::click_or_drag(),
move |client: &mut Client, data| {
let now = Instant::now();
let recent = (now - client.last_click) < Duration::from_millis(300);
client.last_click = now;
client.ui.text(&id).select(
data.cursor,
data.size,
data.sense.is_dragging(),
recent,
);
if let Some(region) = client.ui.window_region(&id) {
client.window.set_ime_allowed(true);
client.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()),
LogicalSize::<f32>::from(region.size().tuple()),
);
}
client.focus = Some(id.clone());
},
);
}
}
impl Client {
pub fn new(window: Arc<Window>) -> Self {
let renderer = Renderer::new(window.clone());
let mut ui = Ui::new();
fn new(mut ui: DefaultUi, _proxy: Proxy<Self::Event>) -> Self {
let rrect = rect(Color::WHITE).radius(20);
let pad_test = (
rrect.color(Color::BLUE),
@@ -103,7 +41,7 @@ impl Client {
.width(rest(3)),
)
.span(Dir::RIGHT)
.add_static(&mut ui);
.add(&mut ui);
let span_test = (
rrect.color(Color::GREEN).width(100),
@@ -114,9 +52,10 @@ impl Client {
rrect.color(Color::RED).width(100),
)
.span(Dir::LEFT)
.add_static(&mut ui);
.add(&mut ui);
let span_add = Span::empty(Dir::RIGHT).add_static(&mut ui);
let span_add = Span::empty(Dir::RIGHT).add(&mut ui);
let span_add_ = span_add.clone();
let add_button = rect(Color::LIME)
.radius(30)
@@ -125,22 +64,21 @@ impl Client {
.ui
.add(image(include_bytes!("assets/sungals.png")).center())
.any();
ctx.ui[span_add].children.push(child);
ctx.ui[&span_add_].children.push(child);
})
.sized((150, 150))
.align(Align::BOT_RIGHT);
let span_add_ = span_add.clone();
let del_button = rect(Color::RED)
.radius(30)
.on(CursorSense::click(), move |ctx: &mut Client, _| {
ctx.ui[span_add].children.pop();
ctx.ui[&span_add_].children.pop();
})
.sized((150, 150))
.align(Align::BOT_LEFT);
let span_add_test = (span_add, add_button, del_button)
.stack()
.add_static(&mut ui);
let span_add_test = (span_add, add_button, del_button).stack().add(&mut ui);
let btext = |content| wtext(content).size(30);
@@ -163,10 +101,10 @@ impl Client {
wtext("pretty cool right?").size(50),
)
.span(Dir::DOWN)
.add_static(&mut ui);
.add(&mut ui);
let texts = Span::empty(Dir::DOWN).gap(10).add_static(&mut ui);
let msg_area = texts.scroll().masked().background(rect(Color::SKY));
let texts = Span::empty(Dir::DOWN).gap(10).add(&mut ui);
let msg_area = texts.clone().scroll().masked().background(rect(Color::SKY));
let add_text = wtext("add")
.editable(false)
.text_align(Align::LEFT)
@@ -183,7 +121,7 @@ impl Client {
let msg_box = text
.background(rect(Color::WHITE.darker(0.5)))
.add(&mut client.ui);
client.ui[texts].children.push(msg_box.any());
client.ui[&texts].children.push(msg_box.any());
})
.add(&mut ui);
let text_edit_scroll = (
@@ -207,14 +145,15 @@ impl Client {
.align(Align::BOT),
)
.span(Dir::DOWN)
.add_static(&mut ui);
.add(&mut ui);
let main = pad_test.pad(10).add_static(&mut ui);
let main = pad_test.clone().pad(10).add(&mut ui);
let switch_button = |color, to, label| {
let switch_button = |color, to: WidgetId, label| {
let main_ = main.clone();
let rect = rect(color)
.id_on(CursorSense::click(), move |id, ui: &mut Ui, _| {
ui[main].inner.set_static(to);
ui[&main_.clone()].inner = to.clone();
ui[id].color = color.darker(0.3);
})
.edit_on(
@@ -249,109 +188,26 @@ impl Client {
.stack()
.set_root(&mut ui);
Self {
renderer,
window,
input: Input::default(),
ui,
info,
focus: None,
clipboard: Clipboard::new().unwrap(),
ime: 0,
last_click: Instant::now(),
}
Self { ui, info }
}
pub fn event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let input_changed = self.input.event(&event);
let cursor_state = self.cursor_state().clone();
let old = self.focus.clone();
if cursor_state.buttons.left.is_start() {
self.focus = None;
}
if input_changed {
let window_size = self.window_size();
self.run_sensors(&cursor_state, window_size);
self.ui.run_sensors(&cursor_state, window_size);
}
if old != self.focus
&& let Some(old) = old
{
self.ui.text(&old).deselect();
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
self.ui.update();
self.renderer.update(&mut self.ui);
self.renderer.draw()
}
WindowEvent::Resized(size) => {
self.ui.resize((size.width, size.height));
self.renderer.resize(&size)
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = &self.focus
&& event.state.is_pressed()
{
let mut text = self.ui.text(sel);
match text.apply_event(&event, &self.input.modifiers) {
TextInputResult::Unfocus => {
self.focus = None;
}
TextInputResult::Submit => {
self.run_event(&sel.clone(), Submit, ());
}
TextInputResult::Paste => {
if let Ok(t) = self.clipboard.get_text() {
text.insert(&t);
}
}
TextInputResult::Copy(text) => {
if let Err(err) = self.clipboard.set_text(text) {
eprintln!("failed to copy text to clipboard: {err}")
}
}
TextInputResult::Unused | TextInputResult::Used => (),
}
}
}
WindowEvent::Ime(ime) => {
if let Some(sel) = &self.focus {
let mut text = self.ui.text(sel);
match ime {
Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => {
// TODO: highlight once that's real
text.replace(self.ime, &content);
self.ime = content.chars().count();
}
Ime::Commit(content) => {
text.insert(&content);
}
}
}
}
_ => (),
fn ui(&mut self) -> &mut DefaultUi {
&mut self.ui
}
fn window_event(&mut self, _: WindowEvent) {
let new = format!(
"widgets: {}\nactive:{}\nviews: {}",
self.ui.num_widgets(),
self.ui.active_widgets(),
self.renderer.ui.view_count()
self.ui.renderer.ui.view_count()
);
if new != *self.ui[&self.info].content {
*self.ui[&self.info].content = new;
}
if self.ui.needs_redraw() {
self.renderer.window().request_redraw();
self.ui.window.request_redraw();
}
self.input.end_frame();
}
}
impl UiCtx for Client {
fn ui(&mut self) -> &mut Ui {
&mut self.ui
self.ui.input.end_frame();
}
}

View File

@@ -69,8 +69,10 @@ impl<W: WidgetLike<Tag>, Tag> Eventable<W::Widget, Tag> for W {
W::Widget: Widget,
{
self.with_id(move |ui, id| {
let id2 = id.clone();
id.on(event, move |ctx, pos| f(&id2, ctx, pos)).add(ui)
// 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)
})
}

View File

@@ -1,12 +1,4 @@
use std::{
any::TypeId,
marker::PhantomData,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc::Sender,
},
};
use std::{any::TypeId, marker::PhantomData, sync::mpsc::Sender};
use crate::{
layout::{Ui, WidgetLike},
@@ -29,7 +21,15 @@ pub struct WidgetId<W = AnyWidget> {
pub(super) id: Id,
counter: RefCounter,
send: Sender<Id>,
is_static: Arc<AtomicBool>,
_pd: PhantomData<W>,
}
#[repr(C)]
pub struct WeakWidgetId<W = AnyWidget> {
pub(super) ty: TypeId,
pub(super) id: Id,
counter: RefCounter,
send: Sender<Id>,
_pd: PhantomData<W>,
}
@@ -39,21 +39,6 @@ impl<W> PartialEq for WidgetId<W> {
}
}
/// A WidgetId for a static widget that cannot be removed from a Ui.
/// Useful because ergonomic clones don't exist yet so you can easily use these in closures.
/// Do not use this if you want the widget to be freeable.
///
/// This is currently not perfectly efficient and just creates new WidgetIds every time it's used,
/// but they don't send drop messages to Ui.
/// Ideally I'd have an enum or something that lets you use either, but that doesn't seem worth it
/// right now; it's good enough and relatively cheap.
#[repr(C)]
pub struct StaticWidgetId<W = AnyWidget> {
pub(super) ty: TypeId,
pub(super) id: Id,
_pd: PhantomData<W>,
}
impl<W> std::fmt::Debug for WidgetId<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
@@ -67,20 +52,18 @@ impl<W> Clone for WidgetId<W> {
ty: self.ty,
counter: self.counter.clone(),
send: self.send.clone(),
is_static: self.is_static.clone(),
_pd: PhantomData,
}
}
}
impl<W> WidgetId<W> {
pub(super) fn new(id: Id, ty: TypeId, send: Sender<Id>, is_static: bool) -> Self {
pub(super) fn new(id: Id, ty: TypeId, send: Sender<Id>) -> Self {
Self {
ty,
id,
counter: RefCounter::new(),
send,
is_static: Arc::new(is_static.into()),
_pd: PhantomData,
}
}
@@ -107,29 +90,47 @@ impl<W> WidgetId<W> {
self.counter.refs()
}
pub fn into_static(self) -> StaticWidgetId<W> {
self.is_static.store(true, Ordering::Release);
StaticWidgetId {
ty: self.ty,
id: self.id,
_pd: PhantomData,
pub fn weak(&self) -> WeakWidgetId<W> {
let Self {
ty,
id,
ref counter,
ref send,
_pd,
} = *self;
WeakWidgetId {
ty,
id,
counter: counter.quiet_clone(),
send: send.clone(),
_pd,
}
}
}
impl WidgetId {
pub fn set_static<W>(&mut self, other: StaticWidgetId<W>) {
let send = self.send.clone();
drop(std::mem::replace(
self,
Self::new(other.id, self.ty, send, true),
));
impl<W> WeakWidgetId<W> {
/// should guarantee that widget is still valid to prevent indexing failures
pub(crate) fn strong(&self) -> WidgetId<W> {
let Self {
ty,
id,
ref counter,
ref send,
_pd,
} = *self;
WidgetId {
ty,
id,
counter: counter.clone(),
send: send.clone(),
_pd,
}
}
}
impl<W> Drop for WidgetId<W> {
fn drop(&mut self) {
if self.counter.drop() && !self.is_static.load(Ordering::Acquire) {
if self.counter.drop() {
let _ = self.send.send(self.id);
}
}
@@ -158,31 +159,6 @@ impl<W: 'static, F: FnOnce(&mut Ui) -> WidgetId<W>> WidgetLike<IdFnTag> for F {
}
}
impl<W> StaticWidgetId<W> {
pub fn to_id(&self, send: &Sender<Id>) -> WidgetId<W> {
WidgetId::new(self.id, self.ty, send.clone(), true)
}
pub fn any(self) -> StaticWidgetId<AnyWidget> {
// SAFETY: self is repr(C)
unsafe { std::mem::transmute(self) }
}
}
impl<W: 'static> WidgetLike<IdTag> for StaticWidgetId<W> {
type Widget = W;
fn add(self, ui: &mut Ui) -> WidgetId<W> {
self.id(&ui.send)
}
}
impl<W> Clone for StaticWidgetId<W> {
fn clone(&self) -> Self {
*self
}
}
impl<W> Copy for StaticWidgetId<W> {}
pub trait WidgetIdLike<W> {
fn id(self, send: &Sender<Id>) -> WidgetId<W>;
}
@@ -193,12 +169,6 @@ impl<W> WidgetIdLike<W> for &WidgetId<W> {
}
}
impl<W> WidgetIdLike<W> for StaticWidgetId<W> {
fn id(self, send: &Sender<Id>) -> WidgetId<W> {
self.to_id(send)
}
}
pub trait IdLike<W> {
fn id(&self) -> Id;
}
@@ -208,9 +178,3 @@ impl<W> IdLike<W> for WidgetId<W> {
self.id
}
}
impl<W> IdLike<W> for StaticWidgetId<W> {
fn id(&self) -> Id {
self.id
}
}

View File

@@ -3,8 +3,8 @@ use image::DynamicImage;
use crate::{
core::{TextEdit, TextEditCtx},
layout::{
Event, EventFn, EventModule, IdLike, PainterData, PixelRegion, StaticWidgetId,
TextureHandle, Vec2, Widget, WidgetId, WidgetInstance, WidgetLike,
Event, EventFn, EventModule, IdLike, PainterData, PixelRegion, TextureHandle, Vec2, Widget,
WidgetId, WidgetInstance, WidgetLike,
},
util::{HashSet, Id},
};
@@ -30,14 +30,6 @@ impl Ui {
w.add(self)
}
pub fn add_static<W: Widget, Tag>(
&mut self,
w: impl WidgetLike<Tag, Widget = W>,
) -> StaticWidgetId<W> {
let id = w.add(self);
id.into_static()
}
/// useful for debugging
pub fn set_label<W>(&mut self, id: &WidgetId<W>, label: String) {
self.data.widgets.data_mut(&id.id).unwrap().label = label;
@@ -79,7 +71,6 @@ impl Ui {
self.data.widgets.reserve(),
TypeId::of::<W>(),
self.send.clone(),
false,
)
}
@@ -215,21 +206,6 @@ impl<W: Widget> IndexMut<&WidgetId<W>> for Ui {
}
}
impl<W: Widget> Index<StaticWidgetId<W>> for Ui {
type Output = W;
fn index(&self, id: StaticWidgetId<W>) -> &Self::Output {
self.data.widgets.get(&id).unwrap()
}
}
impl<W: Widget> IndexMut<StaticWidgetId<W>> for Ui {
fn index_mut(&mut self, id: StaticWidgetId<W>) -> &mut Self::Output {
self.updates.insert(id.id);
self.data.widgets.get_mut(&id).unwrap()
}
}
impl dyn Widget {
pub fn as_any(&self) -> &dyn Any {
self

View File

@@ -1,6 +1,6 @@
use crate::{
core::WidgetPtr,
layout::{Len, Painter, SizeCtx, StaticWidgetId, Ui, WidgetId, WidgetIdFn},
layout::{Len, Painter, SizeCtx, Ui, WidgetId, WidgetIdFn},
};
use std::{any::Any, marker::PhantomData};
@@ -26,7 +26,9 @@ pub struct FnTag;
pub trait WidgetLike<Tag> {
type Widget: 'static;
fn add(self, ui: &mut Ui) -> WidgetId<Self::Widget>;
fn with_id<W2>(
self,
f: impl FnOnce(&mut Ui, WidgetId<Self::Widget>) -> WidgetId<W2>,
@@ -39,18 +41,14 @@ pub trait WidgetLike<Tag> {
f(ui, id)
}
}
fn add_static(self, ui: &mut Ui) -> StaticWidgetId<Self::Widget>
where
Self: Sized,
{
self.add(ui).into_static()
}
fn set_root(self, ui: &mut Ui)
where
Self: Sized,
{
ui.set_root(self);
}
fn set_ptr(self, ptr: &WidgetId<WidgetPtr>, ui: &mut Ui)
where
Self: Sized,

View File

@@ -14,6 +14,7 @@ pub mod core;
pub mod layout;
pub mod render;
pub mod util;
pub mod winit;
pub mod prelude {
pub use crate::core::*;

View File

@@ -22,7 +22,7 @@ pub use primitive::*;
const SHAPE_SHADER: &str = include_str!("./shader.wgsl");
pub struct UiRenderer {
pub struct UiRenderNode {
uniform_group: BindGroup,
primitive_layout: BindGroupLayout,
rsc_layout: BindGroupLayout,
@@ -43,7 +43,7 @@ struct RenderLayer {
primitive_group: BindGroup,
}
impl UiRenderer {
impl UiRenderNode {
pub fn draw<'a>(&'a self, pass: &mut RenderPass<'a>) {
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.uniform_group, &[]);

View File

@@ -17,6 +17,9 @@ impl RefCounter {
let refs = self.0.fetch_sub(1, Ordering::Release);
refs == 0
}
pub fn quiet_clone(&self) -> Self {
Self(self.0.clone())
}
}
impl Clone for RefCounter {

53
src/winit/app.rs Normal file
View File

@@ -0,0 +1,53 @@
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
window::WindowId,
};
pub trait UiState {
type Event: 'static;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self;
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop);
fn event(&mut self, event: Self::Event, event_loop: &ActiveEventLoop);
fn exit(&mut self);
}
pub struct UiApp<State: UiState> {
state: Option<State>,
proxy: EventLoopProxy<State::Event>,
}
impl<State: UiState> UiApp<State> {
pub fn run() {
let event_loop = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy();
event_loop
.run_app(&mut UiApp::<State> { state: None, proxy })
.unwrap();
}
}
impl<State: UiState> ApplicationHandler<State::Event> for UiApp<State> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.state.is_none() {
let state = State::new(event_loop, self.proxy.clone());
self.state = Some(state);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let state = self.state.as_mut().unwrap();
state.window_event(event, event_loop);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: State::Event) {
let state = self.state.as_mut().unwrap();
state.event(event, event_loop);
}
fn exiting(&mut self, _: &ActiveEventLoop) {
let state = self.state.as_mut().unwrap();
state.exit();
}
}

59
src/winit/attr.rs Normal file
View File

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

15
src/winit/event.rs Normal file
View File

@@ -0,0 +1,15 @@
use crate::layout::DefaultEvent;
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Submit;
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Edited;
impl DefaultEvent for Submit {
type Data = ();
}
impl DefaultEvent for Edited {
type Data = ();
}

View File

@@ -1,7 +1,7 @@
use crate::Client;
use iris::{
use crate::{
core::{CursorState, Modifiers},
layout::Vec2,
winit::DefaultUi,
};
use winit::{
event::{MouseButton, MouseScrollDelta, WindowEvent},
@@ -32,10 +32,14 @@ impl Input {
}
}
WindowEvent::MouseWheel { delta, .. } => {
let delta = match *delta {
let mut delta = match *delta {
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
};
if delta.x == 0.0 && self.modifiers.shift {
delta.x = delta.y;
delta.y = 0.0;
}
self.cursor.scroll_delta = delta;
}
WindowEvent::CursorLeft { .. } => {
@@ -66,7 +70,7 @@ impl Input {
}
}
impl Client {
impl DefaultUi {
pub fn window_size(&self) -> Vec2 {
let size = self.renderer.window().inner_size();
(size.width, size.height).into()

188
src/winit/mod.rs Normal file
View File

@@ -0,0 +1,188 @@
use crate::prelude::*;
use crate::winit::event::{Edited, Submit};
use crate::winit::{input::Input, render::UiRenderer};
use arboard::Clipboard;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use std::time::Instant;
use winit::event::{Ime, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
use winit::window::Window;
mod app;
pub mod attr;
pub mod event;
pub mod input;
pub mod render;
pub use app::*;
pub type Proxy<Event> = EventLoopProxy<Event>;
pub struct DefaultUi {
pub renderer: UiRenderer,
pub input: Input,
pub ui: Ui,
pub focus: Option<WidgetId<TextEdit>>,
pub clipboard: Clipboard,
pub window: Arc<Window>,
pub ime: usize,
pub last_click: Instant,
}
pub trait DefaultUiState: 'static {
type Event: 'static;
fn new(ui: DefaultUi, proxy: Proxy<Self::Event>) -> Self;
fn ui(&mut self) -> &mut DefaultUi;
#[allow(unused_variables)]
fn event(&mut self, event: Self::Event) {}
fn exit(&mut self) {}
#[allow(unused_variables)]
fn window_event(&mut self, event: WindowEvent) {}
}
impl<State: DefaultUiState> UiState for State {
type Event = State::Event;
fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<Self::Event>) -> Self {
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title("OPENWORM"))
.unwrap(),
);
let ui = DefaultUi {
renderer: UiRenderer::new(window.clone()),
window,
input: Input::default(),
ui: Ui::new(),
clipboard: Clipboard::new().unwrap(),
ime: 0,
last_click: Instant::now(),
focus: None,
};
State::new(ui, proxy)
}
fn event(&mut self, event: Self::Event, _: &ActiveEventLoop) {
self.event(event);
}
fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let ui = self.ui();
let input_changed = ui.input.event(&event);
let cursor_state = ui.cursor_state().clone();
let old = ui.focus.clone();
if cursor_state.buttons.left.is_start() {
ui.focus = None;
}
if input_changed {
let window_size = ui.window_size();
ui.run_sensors(&cursor_state, window_size);
ui.ui.run_sensors(&cursor_state, window_size);
self.run_sensors(&cursor_state, window_size);
}
let ui = self.ui();
if old != ui.focus
&& let Some(old) = old
{
ui.text(&old).deselect();
}
match &event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
ui.ui.update();
ui.renderer.update(&mut ui.ui);
ui.renderer.draw();
}
WindowEvent::Resized(size) => {
ui.ui.resize((size.width, size.height));
ui.renderer.resize(size)
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = &ui.focus
&& event.state.is_pressed()
{
let sel = &sel.clone();
let mut text = ui.ui.text(sel);
match text.apply_event(event, &ui.input.modifiers) {
TextInputResult::Unfocus => {
ui.focus = None;
ui.window.set_ime_allowed(false);
}
TextInputResult::Submit => {
self.run_event(sel, Submit, ());
}
TextInputResult::Paste => {
if let Ok(t) = ui.clipboard.get_text() {
text.insert(&t);
}
self.run_event(sel, Edited, ());
}
TextInputResult::Copy(text) => {
if let Err(err) = ui.clipboard.set_text(text) {
eprintln!("failed to copy text to clipboard: {err}")
}
}
TextInputResult::Used => {
self.run_event(sel, Edited, ());
}
TextInputResult::Unused => {}
}
}
}
WindowEvent::Ime(ime) => {
if let Some(sel) = &ui.focus {
let mut text = ui.ui.text(sel);
match ime {
Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => {
// TODO: highlight once that's real
text.replace(ui.ime, content);
ui.ime = content.chars().count();
}
Ime::Commit(content) => {
text.insert(content);
}
}
}
}
_ => (),
}
self.window_event(event);
let ui = self.ui();
if ui.needs_redraw() {
ui.renderer.window().request_redraw();
}
ui.input.end_frame();
}
fn exit(&mut self) {
self.exit();
}
}
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
}
}

View File

@@ -1,15 +1,15 @@
use crate::{
layout::Ui,
render::{UiLimits, UiRenderNode},
};
use pollster::FutureExt;
use std::sync::Arc;
use iris::{
layout::Ui,
render::{UiLimits, UiRenderer},
};
use wgpu::{util::StagingBelt, *};
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: Color = Color::BLACK;
pub struct Renderer {
pub struct UiRenderer {
window: Arc<Window>,
surface: Surface<'static>,
device: Device,
@@ -17,10 +17,10 @@ pub struct Renderer {
config: SurfaceConfiguration,
encoder: CommandEncoder,
staging_belt: StagingBelt,
pub ui: UiRenderer,
pub ui: UiRenderNode,
}
impl Renderer {
impl UiRenderer {
pub fn update(&mut self, updates: &mut Ui) {
self.ui.update(&self.device, &self.queue, updates);
}
@@ -72,6 +72,7 @@ impl Renderer {
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
flags: InstanceFlags::empty(),
..Default::default()
});
@@ -100,6 +101,7 @@ impl Renderer {
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
max_buffer_size: 1 << 30,
..Default::default()
},
..Default::default()
@@ -131,7 +133,7 @@ impl Renderer {
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UiRenderer::new(&device, &queue, &config, ui_limits);
let shape_pipeline = UiRenderNode::new(&device, &queue, &config, ui_limits);
Self {
surface,