Files
wayvr/wlx-overlay-s/src/gui/panel/mod.rs
2025-12-20 16:42:04 +09:00

428 lines
13 KiB
Rust

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<S> =
Box<dyn Fn(&mut GuiPanel<S>, &mut AppState, OverlayEventData) -> anyhow::Result<()>>;
pub struct GuiPanel<S> {
pub layout: Layout,
pub state: S,
pub timers: Vec<GuiTimer>,
pub parser_state: ParserState,
pub max_size: Vec2,
pub gui_scale: f32,
pub on_notify: Option<OnNotifyFunc<S>>,
pub initialized: bool,
interaction_transform: Option<Affine2>,
context: WguiContext,
timestep: Timestep,
has_focus: [bool; 2],
last_content_size: Vec2,
}
pub type OnCustomIdFunc<S> = Box<
dyn Fn(
Rc<str>,
WidgetID,
&wgui::parser::ParseDocumentParams,
&mut Layout,
&mut ParserState,
&mut S,
) -> anyhow::Result<()>,
>;
pub type OnCustomAttribFunc = Box<dyn Fn(&mut Layout, &CustomAttribsInfoOwned, &AppState)>;
pub struct NewGuiPanelParams<S> {
pub on_custom_id: Option<OnCustomIdFunc<S>>, // used only in `new_from_template`
pub on_custom_attrib: Option<OnCustomAttribFunc>, // used only in `new_from_template`
pub resize_to_parent: bool,
pub external_xml: bool,
pub gui_scale: f32,
}
impl<S> Default for NewGuiPanelParams<S> {
fn default() -> Self {
Self {
on_custom_id: None,
on_custom_attrib: None,
resize_to_parent: false,
external_xml: false,
gui_scale: 1.0,
}
}
}
impl<S: 'static> GuiPanel<S> {
pub fn new_from_template(
app: &mut AppState,
path: &str,
mut state: S,
params: NewGuiPanelParams<S>,
) -> anyhow::Result<Self> {
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::<WidgetLabel>(elem.widget_id)
.is_some()
{
setup_custom_label::<S>(&mut layout, elem, app);
} else if let Ok(button) =
parser_state.fetch_component_from_widget_id_as::<ComponentButton>(elem.widget_id)
{
setup_custom_button::<S>(&mut layout, elem, app, button);
}
if let Some(on_custom_attrib) = &params.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<S>,
) -> anyhow::Result<Self> {
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<AppState, S>,
) -> Option<EventListenerID> {
self.layout.add_event_listener(widget_id, kind, callback)
}
}
impl<S: 'static> OverlayBackend for GuiPanel<S> {
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<ShouldRender> {
//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<FrameMeta> {
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<Affine2> {
self.interaction_transform
}
fn get_attrib(&self, _attrib: BackendAttrib) -> Option<BackendAttribValue> {
None
}
fn set_attrib(&mut self, _app: &mut AppState, _value: BackendAttribValue) -> bool {
false
}
}