diff --git a/readme.md b/readme.md index efa14c0..6278d4f 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/src/bin/test/app.rs b/src/bin/test/app.rs deleted file mode 100644 index 16c154f..0000000 --- a/src/bin/test/app.rs +++ /dev/null @@ -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, -} - -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); - } -} diff --git a/src/bin/test/main.rs b/src/bin/test/main.rs index 1da7e7a..6916d8d 100644 --- a/src/bin/test/main.rs +++ b/src/bin/test/main.rs @@ -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::::run(); } pub struct Client { - renderer: Renderer, - window: Arc, - input: Input, - ui: Ui, + ui: DefaultUi, info: WidgetId, - focus: Option>, - 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 for Selectable { - type Input = (); - - fn run(ui: &mut Ui, id: &WidgetId, _: 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::::from(region.top_left.tuple()), - LogicalSize::::from(region.size().tuple()), - ); - } - client.focus = Some(id.clone()); - }, - ); - } -} - -impl Client { - pub fn new(window: Arc) -> Self { - let renderer = Renderer::new(window.clone()); - - let mut ui = Ui::new(); + fn new(mut ui: DefaultUi, _proxy: Proxy) -> Self { let rrect = rect(Color::WHITE).radius(20); let pad_test = ( rrect.color(Color::BLUE), @@ -250,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(); } } diff --git a/src/lib.rs b/src/lib.rs index cb1e028..b8906f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::*; diff --git a/src/render/mod.rs b/src/render/mod.rs index 06621e9..5f428ed 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -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, &[]); diff --git a/src/winit/app.rs b/src/winit/app.rs new file mode 100644 index 0000000..33be8af --- /dev/null +++ b/src/winit/app.rs @@ -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; + 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: Option, + proxy: EventLoopProxy, +} + +impl UiApp { + 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: None, proxy }) + .unwrap(); + } +} + +impl ApplicationHandler for UiApp { + 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(); + } +} diff --git a/src/winit/attr.rs b/src/winit/attr.rs new file mode 100644 index 0000000..057fcec --- /dev/null +++ b/src/winit/attr.rs @@ -0,0 +1,59 @@ +use crate::{prelude::*, winit::DefaultUi}; +use std::time::{Duration, Instant}; +use winit::dpi::{LogicalPosition, LogicalSize}; + +pub struct Selector; + +impl WidgetAttr for Selector { + type Input = WidgetId; + + fn run(ui: &mut Ui, container: &WidgetId, 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 for Selectable { + type Input = (); + + fn run(ui: &mut Ui, id: &WidgetId, _: 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, 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::::from(region.top_left.tuple()), + LogicalSize::::from(region.size().tuple()), + ); + } + ui.focus = Some(id); +} diff --git a/src/winit/event.rs b/src/winit/event.rs new file mode 100644 index 0000000..51cc925 --- /dev/null +++ b/src/winit/event.rs @@ -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 = (); +} diff --git a/src/bin/test/input.rs b/src/winit/input.rs similarity index 90% rename from src/bin/test/input.rs rename to src/winit/input.rs index 2dfb729..4039313 100644 --- a/src/bin/test/input.rs +++ b/src/winit/input.rs @@ -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() diff --git a/src/winit/mod.rs b/src/winit/mod.rs new file mode 100644 index 0000000..7f769df --- /dev/null +++ b/src/winit/mod.rs @@ -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 = EventLoopProxy; + +pub struct DefaultUi { + pub renderer: UiRenderer, + pub input: Input, + pub ui: Ui, + pub focus: Option>, + pub clipboard: Clipboard, + pub window: Arc, + pub ime: usize, + pub last_click: Instant, +} + +pub trait DefaultUiState: 'static { + type Event: 'static; + fn new(ui: DefaultUi, proxy: Proxy) -> 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 UiState for State { + type Event = State::Event; + + fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy) -> 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 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 + } +} diff --git a/src/bin/test/render/mod.rs b/src/winit/render.rs similarity index 94% rename from src/bin/test/render/mod.rs rename to src/winit/render.rs index 03ae849..510d9bc 100644 --- a/src/bin/test/render/mod.rs +++ b/src/winit/render.rs @@ -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, 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,