use std::{cell::RefCell, rc::Rc}; use button::setup_custom_button; use glam::{Affine2, Vec2, vec2}; use label::setup_custom_label; use wgui::{ assets::AssetPath, components::button::ComponentButton, drawing, event::{ Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, InternalStateChangeEvent, MouseButtonIndex, MouseDownEvent, MouseLeaveEvent, MouseMotionEvent, MouseUpEvent, MouseWheelEvent, }, gfx::cmd::WGfxClearMode, layout::{Layout, LayoutParams, WidgetID}, parser::{CustomAttribsInfoOwned, Fetchable, ParserState}, renderer_vk::context::Context as WguiContext, widget::{EventResult, label::WidgetLabel}, }; use wlx_common::timestep::Timestep; use crate::{ backend::input::{Haptics, HoverResult, PointerHit, PointerMode}, state::AppState, subsystem::hid::WheelDelta, windowing::backend::{ BackendAttrib, BackendAttribValue, FrameMeta, OverlayBackend, OverlayEventData, RenderResources, ShouldRender, ui_transform, }, }; use super::timer::GuiTimer; pub mod button; mod helper; mod label; const DEFAULT_MAX_SIZE: f32 = 2048.0; const COLOR_ERR: drawing::Color = drawing::Color::new(1., 0., 1., 1.); pub type OnNotifyFunc = Box, &mut AppState, OverlayEventData) -> anyhow::Result<()>>; pub struct GuiPanel { pub layout: Layout, pub state: S, pub timers: Vec, pub parser_state: ParserState, pub max_size: Vec2, pub gui_scale: f32, pub on_notify: Option>, pub initialized: bool, interaction_transform: Option, context: WguiContext, timestep: Timestep, has_focus: [bool; 2], last_content_size: Vec2, } pub type OnCustomIdFunc = Box< dyn Fn( Rc, WidgetID, &wgui::parser::ParseDocumentParams, &mut Layout, &mut ParserState, &mut S, ) -> anyhow::Result<()>, >; pub type OnCustomAttribFunc = Box; pub struct NewGuiPanelParams { pub on_custom_id: Option>, // used only in `new_from_template` pub on_custom_attrib: Option, // used only in `new_from_template` pub resize_to_parent: bool, pub external_xml: bool, pub gui_scale: f32, } impl Default for NewGuiPanelParams { fn default() -> Self { Self { on_custom_id: None, on_custom_attrib: None, resize_to_parent: false, external_xml: false, gui_scale: 1.0, } } } impl GuiPanel { pub fn new_from_template( app: &mut AppState, path: &str, mut state: S, params: NewGuiPanelParams, ) -> anyhow::Result { let custom_elems = Rc::new(RefCell::new(vec![])); let doc_params = wgui::parser::ParseDocumentParams { globals: app.wgui_globals.clone(), path: params .external_xml .then_some(AssetPath::File(path)) .unwrap_or(AssetPath::FileOrBuiltIn(path)), extra: wgui::parser::ParseDocumentExtra { on_custom_attribs: Some(Box::new({ let custom_elems = custom_elems.clone(); move |attribs| { custom_elems.borrow_mut().push(attribs.to_owned()); } })), ..Default::default() }, }; let (mut layout, mut parser_state) = wgui::parser::new_layout_from_assets( &doc_params, &LayoutParams { resize_to_parent: params.resize_to_parent, }, )?; if let Some(on_element_id) = params.on_custom_id { let ids = parser_state.data.ids.clone(); // FIXME: copying all ids? for (id, widget) in ids { on_element_id( id.clone(), widget, &doc_params, &mut layout, &mut parser_state, &mut state, )?; } } for elem in custom_elems.borrow().iter() { if layout .state .widgets .get_as::(elem.widget_id) .is_some() { setup_custom_label::(&mut layout, elem, app); } else if let Ok(button) = parser_state.fetch_component_from_widget_id_as::(elem.widget_id) { setup_custom_button::(&mut layout, elem, app, button); } if let Some(on_custom_attrib) = ¶ms.on_custom_attrib { on_custom_attrib(&mut layout, elem, app); } } let context = WguiContext::new(&mut app.wgui_shared, 1.0)?; let mut timestep = Timestep::new(); timestep.set_tps(60.0); Ok(Self { layout, context, timestep, state, parser_state, max_size: vec2(DEFAULT_MAX_SIZE as _, DEFAULT_MAX_SIZE as _), timers: vec![], interaction_transform: None, on_notify: None, gui_scale: params.gui_scale, initialized: false, has_focus: [false, false], last_content_size: Vec2::ZERO, }) } pub fn new_blank( app: &mut AppState, state: S, params: NewGuiPanelParams, ) -> anyhow::Result { let layout = Layout::new( app.wgui_globals.clone(), &LayoutParams { resize_to_parent: params.resize_to_parent, }, )?; let context = WguiContext::new(&mut app.wgui_shared, 1.0)?; let mut timestep = Timestep::new(); timestep.set_tps(60.0); Ok(Self { layout, context, timestep, state, parser_state: ParserState::default(), max_size: vec2(DEFAULT_MAX_SIZE as _, DEFAULT_MAX_SIZE as _), timers: vec![], on_notify: None, interaction_transform: None, gui_scale: params.gui_scale, initialized: false, has_focus: [false, false], last_content_size: Vec2::ZERO, }) } pub fn update_layout(&mut self) -> anyhow::Result<()> { self.layout.update(self.max_size, 0.0) } pub fn push_event(&mut self, app: &mut AppState, event: &WguiEvent) -> EventResult { match self.layout.push_event(event, app, &mut self.state) { Ok(r) => r, Err(e) => { log::error!("Failed to push event: {e:?}"); EventResult::NoHit } } } pub fn add_event_listener( &mut self, widget_id: WidgetID, kind: EventListenerKind, callback: EventCallback, ) -> Option { self.layout.add_event_listener(widget_id, kind, callback) } } impl OverlayBackend for GuiPanel { fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> { if self.layout.content_size.x * self.layout.content_size.y != 0.0 { self.update_layout()?; self.interaction_transform = Some(ui_transform([ self.layout.content_size.x as _, self.layout.content_size.y as _, ])); self.initialized = true; } Ok(()) } fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> { Ok(()) } fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> { self.layout.needs_redraw = true; self.timestep.reset(); Ok(()) } fn should_render(&mut self, app: &mut AppState) -> anyhow::Result { //TODO: this only executes one timer event per frame if let Some(signal) = self.timers.iter_mut().find_map(GuiTimer::check_tick) { self.push_event( app, &WguiEvent::InternalStateChange(InternalStateChangeEvent { metadata: signal }), ); } while self.timestep.on_tick() { self.layout.tick()?; } if self.layout.content_size.x * self.layout.content_size.y == 0.0 { log::trace!("Unable to render: content size 0"); return Ok(ShouldRender::Unable); } if !self .last_content_size .abs_diff_eq(self.layout.content_size, 0.1 /* pixels */) { self.interaction_transform = Some(ui_transform([ self.layout.content_size.x as _, self.layout.content_size.y as _, ])); self.last_content_size = self.layout.content_size; } Ok(if self.layout.check_toggle_needs_redraw() { ShouldRender::Should } else { ShouldRender::Can }) } fn render(&mut self, app: &mut AppState, rdr: &mut RenderResources) -> anyhow::Result<()> { self.context .update_viewport(&mut app.wgui_shared, rdr.extent, self.gui_scale)?; self.layout .update(self.max_size / self.gui_scale, self.timestep.alpha)?; let globals = self.layout.state.globals.clone(); // sorry let mut globals = globals.get(); let primitives = wgui::drawing::draw(&mut wgui::drawing::DrawParams { globals: &mut globals, layout: &mut self.layout, debug_draw: false, timestep_alpha: self.timestep.alpha, })?; self.context.draw( &globals.font_system, &mut app.wgui_shared, &mut rdr.cmd_buf_single(), &primitives, )?; Ok(()) } fn frame_meta(&mut self) -> Option { Some(FrameMeta { clear: WGfxClearMode::Clear([0., 0., 0., 0.]), extent: [ self.max_size.x.min(self.layout.content_size.x) as _, self.max_size.y.min(self.layout.content_size.y) as _, 1, ], ..Default::default() }) } fn notify(&mut self, app: &mut AppState, data: OverlayEventData) -> anyhow::Result<()> { let Some(on_notify) = self.on_notify.take() else { return Ok(()); }; on_notify(self, app, data)?; self.on_notify = Some(on_notify); Ok(()) } fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: WheelDelta) { let e = WguiEvent::MouseWheel(MouseWheelEvent { delta: vec2(delta.x, delta.y), pos: hit.uv * self.layout.content_size, device: hit.pointer, }); self.push_event(app, &e); } fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult { let e = &WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv * self.layout.content_size, device: hit.pointer, }); self.has_focus[hit.pointer] = true; let result = self.push_event(app, e); HoverResult { consume: result != EventResult::NoHit, haptics: self .layout .check_toggle_haptics_triggered() .then_some(Haptics { intensity: 0.1, duration: 0.01, frequency: 5.0, }), } } fn on_left(&mut self, app: &mut AppState, pointer: usize) { let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); self.has_focus[pointer] = false; self.push_event(app, &e); } fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) { let index = match hit.mode { PointerMode::Left => MouseButtonIndex::Left, PointerMode::Right => MouseButtonIndex::Right, PointerMode::Middle => MouseButtonIndex::Middle, _ => return, }; let e = if pressed { WguiEvent::MouseDown(MouseDownEvent { pos: hit.uv * self.layout.content_size, index, device: hit.pointer, }) } else { WguiEvent::MouseUp(MouseUpEvent { pos: hit.uv * self.layout.content_size, index, device: hit.pointer, }) }; self.push_event(app, &e); // released while off-panel → send mouse leave as well if !pressed && !self.has_focus[hit.pointer] { let e = WguiEvent::MouseMotion(MouseMotionEvent { pos: vec2(-1., -1.), device: hit.pointer, }); self.push_event(app, &e); let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: hit.pointer, }); self.push_event(app, &e); } } fn get_interaction_transform(&mut self) -> Option { self.interaction_transform } fn get_attrib(&self, _attrib: BackendAttrib) -> Option { None } fn set_attrib(&mut self, _app: &mut AppState, _value: BackendAttribValue) -> bool { false } }