use std::{collections::HashMap, rc::Rc}; use kahlo::math::{PixelPoint, PixelSize}; use crate::{ component::Component, font::FontStore, input::MouseButton, layout::{LayoutCache, LayoutNode}, theme::Theme, window::{Window, WindowBuilder, WindowComponent, WindowEvent, WindowStateAccess}, }; #[derive(Debug)] pub enum UIControlMsg { Terminate, } pub trait UIComponent: Sized + Component { fn init(&mut self, ui_handle: &mut UIHandle); fn poll(&mut self, ui_handle: &mut UIHandle) -> Vec; } pub(crate) struct UIState { pub(crate) layout_cache: std::rc::Rc, pub(crate) window_states: HashMap>, pub(crate) fontstore: FontStore, pub(crate) theme: Theme, } pub struct UIHandle<'l> { pub(crate) state: &'l mut UIState, pub(crate) eloop: &'l winit::event_loop::ActiveEventLoop, } impl<'l> UIHandle<'l> { pub fn window_builder<'r>(&'r mut self) -> WindowBuilder<'r, 'l> { WindowBuilder::new(self) } pub fn new_layout_node(&self) -> LayoutNode { LayoutNode::new(self.state.layout_cache.clone()) } pub fn theme(&self) -> &Theme { &self.state.theme } pub fn lookup_font(&self, q: &fontdb::Query) -> Rc { self.state .fontstore .query_for_font(q) .expect("no matching font found") } } pub struct UI { state: UIState, event_loop: Option>, ui_component: UIC, autoclose: bool, initialized: bool, } impl UI { pub fn new(uic: UIC, fontstore: FontStore) -> Self { let lcache = LayoutCache::new(); let eloop = winit::event_loop::EventLoop::builder().build().unwrap(); eloop.set_control_flow(winit::event_loop::ControlFlow::Wait); Self { state: UIState { layout_cache: lcache, window_states: Default::default(), theme: Theme::build_default(&fontstore), fontstore, }, event_loop: Some(eloop), ui_component: uic, autoclose: true, initialized: false, } } pub fn disable_autoclose(mut self) -> Self { self.autoclose = false; self } pub fn ui_component(&self) -> &UIC { &self.ui_component } pub fn ui_component_mut(&mut self) -> &mut UIC { &mut self.ui_component } /// Public helper function, delivers a message to the UI component and then processes whatever /// control messages result. pub fn deliver_msg(&mut self, msg: UIC::Msg) { let cmsgs = self.ui_component.process(msg); self.process_control_msgs(cmsgs.into_iter()); } fn process_control_msgs(&mut self, msgs: impl Iterator) { for cmsg in msgs { match cmsg { UIControlMsg::Terminate => { todo!() // self.ui_running = false; } } } } pub fn run(mut self) { let eloop = self.event_loop.take().unwrap(); eloop.run_app(&mut self).unwrap(); } fn pump_events(&mut self, eloop: &winit::event_loop::ActiveEventLoop) { let evts = self.ui_component.poll(&mut UIHandle { eloop, state: &mut self.state, }); for evt in evts.into_iter() { match evt { UIControlMsg::Terminate => eloop.exit(), } } } } impl winit::application::ApplicationHandler<()> for UI { fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { if !self.initialized { self.ui_component.init(&mut UIHandle { eloop: event_loop, state: &mut self.state, }); self.initialized = true; } } fn window_event( &mut self, event_loop: &winit::event_loop::ActiveEventLoop, window_id: winit::window::WindowId, event: winit::event::WindowEvent, ) { let Some(wsa) = self .state .window_states .get(&window_id) .and_then(std::rc::Weak::upgrade) else { println!("superfluous event: {event:?}"); return; }; match event { winit::event::WindowEvent::CloseRequested => { wsa.push_event(WindowEvent::CloseRequested); self.pump_events(event_loop); } winit::event::WindowEvent::Destroyed => { self.state.window_states.remove(&window_id); } winit::event::WindowEvent::RedrawRequested => { wsa.redraw(&UIHandle { eloop: event_loop, state: &mut self.state, }); self.pump_events(event_loop); } winit::event::WindowEvent::CursorMoved { position, .. } => { wsa.update_mouse_pos(PixelPoint::new(position.x as i32, position.y as i32)); wsa.request_redraw(); self.pump_events(event_loop); } winit::event::WindowEvent::MouseInput { state, button, .. } => { let button = match button { winit::event::MouseButton::Left => MouseButton::Left, winit::event::MouseButton::Right => MouseButton::Right, _ => return, }; wsa.update_mouse_button(button, state.is_pressed()); wsa.request_redraw(); self.pump_events(event_loop); } winit::event::WindowEvent::Resized(newsize) => { wsa.notify_resize(PixelSize::new(newsize.width as i32, newsize.height as i32)); wsa.request_redraw(); } winit::event::WindowEvent::KeyboardInput { device_id, event, is_synthetic, } => { if event.state == winit::event::ElementState::Pressed { if let Some(text) = event.text { wsa.update_text_input(text.as_str()); } wsa.push_keypress(event.logical_key); self.pump_events(event_loop); wsa.request_redraw(); } } _ => {} } } } /// Opinionated base UI component. /// /// Most of the time, this will be the UI component you will want to use: a single main window, /// passing UI events back from the window component. Unless you want multiple windows, or need to /// use a custom font directory, there is no reason to not use this component. pub struct OpinionatedUIComponent<'l, WC: WindowComponent> { window: Option>, builder: Option WC + 'l>>, } impl<'l, WC: WindowComponent> OpinionatedUIComponent<'l, WC> {} impl<'l, WC: WindowComponent> Component for OpinionatedUIComponent<'l, WC> { type ParentMsg = UIControlMsg; type Msg = UIControlMsg; fn process(&mut self, msg: Self::Msg) -> Vec { vec![msg] } } impl<'l, WC: WindowComponent> UIComponent for OpinionatedUIComponent<'l, WC> { fn init(&mut self, ui_handle: &mut UIHandle) { if let Some(builder) = self.builder.take() { self.window = Some(ui_handle.window_builder().build(builder)); } } fn poll(&mut self, uih: &mut UIHandle) -> Vec { if let Some(window) = self.window.as_mut() { window.poll(uih) } else { vec![] } } } /// Build an [`OpinionatedUIComponent`] for a given [`WindowComponent`] that passes /// [`UIControlMsg`]s back to its parent component and uses system fonts. pub fn make_opinionated_ui<'l, WC: WindowComponent>( wc: impl 'l + FnOnce(&mut UIHandle) -> WC, ) -> UI> { let ouic = OpinionatedUIComponent { window: None, builder: Some(Box::new(wc)), }; UI::new(ouic, FontStore::new_with_system_fonts()) }