584 lines
20 KiB
Rust
584 lines
20 KiB
Rust
use std::{
|
|
any::Any,
|
|
cell::RefCell,
|
|
mem::{self, ManuallyDrop},
|
|
rc::Rc,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use glam::vec2;
|
|
use slotmap::Key;
|
|
use wgui::{
|
|
components::{button::ComponentButton, checkbox::ComponentCheckbox, slider::ComponentSlider},
|
|
event::{CallbackDataCommon, EventAlterables, EventCallback},
|
|
parser::Fetchable,
|
|
widget::EventResult,
|
|
};
|
|
use wlx_common::overlays::{BackendAttrib, BackendAttribValue, MouseTransform, StereoMode};
|
|
|
|
use crate::{
|
|
attrib_value,
|
|
backend::{
|
|
input::HoverResult,
|
|
task::{OverlayTask, TaskContainer, TaskType},
|
|
},
|
|
gui::panel::{GuiPanel, NewGuiPanelParams, OnCustomAttribFunc, button::BUTTON_EVENTS},
|
|
overlays::edit::{
|
|
lock::InteractLockHandler,
|
|
mouse::new_mouse_tab_handler,
|
|
pos::{PosTabState, new_pos_tab_handler},
|
|
sprite_tab::SpriteTabHandler,
|
|
stereo::new_stereo_tab_handler,
|
|
tab::ButtonPaneTabSwitcher,
|
|
},
|
|
state::AppState,
|
|
subsystem::hid::WheelDelta,
|
|
windowing::{
|
|
OverlayID, OverlaySelector,
|
|
backend::{DummyBackend, OverlayBackend, OverlayEventData, RenderResources, ShouldRender},
|
|
window::OverlayWindowConfig,
|
|
},
|
|
};
|
|
|
|
mod lock;
|
|
mod mouse;
|
|
mod pos;
|
|
mod sprite_tab;
|
|
mod stereo;
|
|
pub mod tab;
|
|
|
|
struct EditModeState {
|
|
tasks: Rc<RefCell<TaskContainer>>,
|
|
id: Rc<RefCell<OverlayID>>,
|
|
tabs: ButtonPaneTabSwitcher,
|
|
lock: InteractLockHandler,
|
|
pos: SpriteTabHandler<PosTabState>,
|
|
stereo: SpriteTabHandler<StereoMode>,
|
|
mouse: SpriteTabHandler<MouseTransform>,
|
|
}
|
|
|
|
type EditModeWrapPanel = GuiPanel<EditModeState>;
|
|
|
|
#[derive(Default)]
|
|
pub struct EditWrapperManager {
|
|
edit_mode: bool,
|
|
panel_pool: Vec<EditModeWrapPanel>,
|
|
}
|
|
|
|
impl EditWrapperManager {
|
|
pub fn wrap_edit_mode(
|
|
&mut self,
|
|
id: OverlayID,
|
|
owc: &mut OverlayWindowConfig,
|
|
app: &mut AppState,
|
|
) -> anyhow::Result<()> {
|
|
if owc.editing {
|
|
return Ok(());
|
|
}
|
|
|
|
log::debug!("EditMode wrap on {}", owc.name);
|
|
let mut panel = self.panel_pool.pop();
|
|
if panel.is_none() {
|
|
panel = Some(make_edit_panel(app)?);
|
|
}
|
|
let mut panel = panel.unwrap();
|
|
reset_panel(&mut panel, id, owc)?;
|
|
|
|
let inner = mem::replace(&mut owc.backend, Box::new(DummyBackend {}));
|
|
|
|
owc.backend = Box::new(EditModeBackendWrapper {
|
|
inner: ManuallyDrop::new(inner),
|
|
panel: ManuallyDrop::new(panel),
|
|
last_render: Instant::now(),
|
|
can_render_inner: false,
|
|
});
|
|
owc.editing = true;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn unwrap_edit_mode(
|
|
&mut self,
|
|
owc: &mut OverlayWindowConfig,
|
|
app: &mut AppState,
|
|
) -> anyhow::Result<()> {
|
|
if !owc.editing {
|
|
return Ok(());
|
|
}
|
|
|
|
log::debug!("EditMode unwrap on {}", owc.name);
|
|
let wrapper = mem::replace(&mut owc.backend, Box::new(DummyBackend {}));
|
|
let mut wrapper: Box<dyn Any> = wrapper;
|
|
let wrapper = wrapper
|
|
.downcast_mut::<EditModeBackendWrapper>()
|
|
.expect("Wrong type to unwrap");
|
|
|
|
let panel = unsafe { ManuallyDrop::take(&mut wrapper.panel) };
|
|
self.panel_pool.push(panel);
|
|
|
|
let inner = unsafe { ManuallyDrop::take(&mut wrapper.inner) };
|
|
owc.backend = inner;
|
|
owc.editing = false;
|
|
|
|
owc.backend.resume(app)
|
|
|
|
// wrapper is destroyed with nothing left inside
|
|
}
|
|
}
|
|
|
|
pub struct EditModeBackendWrapper {
|
|
panel: ManuallyDrop<EditModeWrapPanel>,
|
|
inner: ManuallyDrop<Box<dyn OverlayBackend>>,
|
|
|
|
last_render: Instant,
|
|
can_render_inner: bool,
|
|
}
|
|
|
|
impl OverlayBackend for EditModeBackendWrapper {
|
|
fn init(&mut self, app: &mut crate::state::AppState) -> anyhow::Result<()> {
|
|
self.inner.init(app)?;
|
|
self.panel.init(app)
|
|
}
|
|
fn pause(&mut self, app: &mut crate::state::AppState) -> anyhow::Result<()> {
|
|
self.inner.pause(app)?;
|
|
self.panel.pause(app)
|
|
}
|
|
fn resume(&mut self, app: &mut crate::state::AppState) -> anyhow::Result<()> {
|
|
self.inner.resume(app)?;
|
|
self.panel.resume(app)
|
|
}
|
|
fn should_render(&mut self, app: &mut crate::state::AppState) -> anyhow::Result<ShouldRender> {
|
|
{
|
|
let mut local_tasks = self.panel.state.tasks.borrow_mut();
|
|
app.tasks.transfer_from(&mut local_tasks);
|
|
}
|
|
|
|
let inner = self.inner.should_render(app)?;
|
|
|
|
if !matches!(inner, ShouldRender::Unable)
|
|
&& let Some(ref frame_meta) = self.inner.frame_meta()
|
|
{
|
|
let (width_px, height_px) = (frame_meta.extent[0], frame_meta.extent[1]);
|
|
|
|
let new_size = vec2(width_px as _, height_px as _);
|
|
if !self.panel.max_size.abs_diff_eq(new_size, 0.1) {
|
|
log::debug!("EditWrapperGui size {} → {new_size}", self.panel.max_size);
|
|
self.panel.max_size = new_size;
|
|
|
|
let gui_scale = (new_size.x / 750.0).min(new_size.y / 300.0);
|
|
self.panel.gui_scale = (gui_scale * 4.0).round() / 4.0;
|
|
self.panel.update_layout()?;
|
|
}
|
|
|
|
self.can_render_inner = true;
|
|
} else {
|
|
self.can_render_inner = false;
|
|
if self.last_render.elapsed() < Duration::from_millis(100) {
|
|
return Ok(ShouldRender::Unable);
|
|
}
|
|
}
|
|
|
|
let panel = self.panel.should_render(app)?;
|
|
|
|
#[allow(clippy::match_same_arms)]
|
|
Ok(match (inner, panel) {
|
|
(ShouldRender::Should, ShouldRender::Should) => ShouldRender::Should,
|
|
(ShouldRender::Should, ShouldRender::Can) => ShouldRender::Should,
|
|
(ShouldRender::Can, ShouldRender::Should) => ShouldRender::Should,
|
|
(ShouldRender::Can, ShouldRender::Can) => ShouldRender::Can,
|
|
// only if last render was more than 100ms ago.
|
|
(ShouldRender::Unable, ShouldRender::Should) => ShouldRender::Should,
|
|
_ => ShouldRender::Unable,
|
|
})
|
|
}
|
|
fn render(
|
|
&mut self,
|
|
app: &mut crate::state::AppState,
|
|
rdr: &mut RenderResources,
|
|
) -> anyhow::Result<()> {
|
|
if self.can_render_inner {
|
|
self.inner.render(app, rdr)?;
|
|
}
|
|
|
|
self.panel.render(app, rdr)?;
|
|
// `GuiPanel` is not stereo-aware, so just render the same pass twice
|
|
if rdr.cmd_bufs.len() > 1 {
|
|
rdr.cmd_bufs.reverse();
|
|
self.panel.render(app, rdr)?;
|
|
rdr.cmd_bufs.reverse();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
fn frame_meta(&mut self) -> Option<crate::windowing::backend::FrameMeta> {
|
|
self.inner.frame_meta()
|
|
}
|
|
fn on_hover(
|
|
&mut self,
|
|
app: &mut crate::state::AppState,
|
|
hit: &crate::backend::input::PointerHit,
|
|
) -> HoverResult {
|
|
// pass through hover events to force pipewire to capture frames for us
|
|
let _ = self.inner.on_hover(app, hit);
|
|
self.panel.on_hover(app, hit)
|
|
}
|
|
fn on_left(&mut self, app: &mut crate::state::AppState, pointer: usize) {
|
|
self.inner.on_left(app, pointer);
|
|
self.panel.on_left(app, pointer);
|
|
}
|
|
fn on_pointer(
|
|
&mut self,
|
|
app: &mut crate::state::AppState,
|
|
hit: &crate::backend::input::PointerHit,
|
|
pressed: bool,
|
|
) {
|
|
self.panel.on_pointer(app, hit, pressed);
|
|
}
|
|
fn on_scroll(
|
|
&mut self,
|
|
app: &mut crate::state::AppState,
|
|
hit: &crate::backend::input::PointerHit,
|
|
delta: WheelDelta,
|
|
) {
|
|
self.panel.on_scroll(app, hit, delta);
|
|
}
|
|
fn notify(&mut self, app: &mut AppState, event_data: OverlayEventData) -> anyhow::Result<()> {
|
|
self.panel.notify(app, event_data)
|
|
}
|
|
fn get_interaction_transform(&mut self) -> Option<glam::Affine2> {
|
|
self.inner.get_interaction_transform()
|
|
}
|
|
fn get_attrib(&self, attrib: BackendAttrib) -> Option<BackendAttribValue> {
|
|
self.inner.get_attrib(attrib)
|
|
}
|
|
fn set_attrib(&mut self, app: &mut AppState, value: BackendAttribValue) -> bool {
|
|
self.inner.set_attrib(app, value)
|
|
}
|
|
}
|
|
|
|
fn make_edit_panel(app: &mut AppState) -> anyhow::Result<EditModeWrapPanel> {
|
|
let state = EditModeState {
|
|
id: Rc::new(RefCell::new(OverlayID::null())),
|
|
tasks: Rc::new(RefCell::new(TaskContainer::new())),
|
|
tabs: ButtonPaneTabSwitcher::default(),
|
|
lock: InteractLockHandler::default(),
|
|
pos: SpriteTabHandler::default(),
|
|
stereo: SpriteTabHandler::default(),
|
|
mouse: SpriteTabHandler::default(),
|
|
};
|
|
|
|
let anim_mult = app.wgui_globals.defaults().animation_mult;
|
|
|
|
let on_custom_attrib: OnCustomAttribFunc = Box::new(move |layout, parser, attribs, _app| {
|
|
let Ok(button) =
|
|
parser.fetch_component_from_widget_id_as::<ComponentButton>(attribs.widget_id)
|
|
else {
|
|
return;
|
|
};
|
|
|
|
for (name, kind, test_button, test_duration) in &BUTTON_EVENTS {
|
|
let Some(action) = attribs.get_value(name) else {
|
|
continue;
|
|
};
|
|
|
|
let mut args = action.split_whitespace();
|
|
let Some(command) = args.next() else {
|
|
continue;
|
|
};
|
|
|
|
let button = button.clone();
|
|
|
|
let callback: EventCallback<AppState, EditModeState> = match command {
|
|
"::EditModeToggleLock" => Box::new(move |common, data, app, state| {
|
|
if !test_button(data) || !test_duration(&button, app) {
|
|
return Ok(EventResult::Pass);
|
|
}
|
|
|
|
let sel = OverlaySelector::Id(*state.id.borrow());
|
|
let task = state.lock.toggle(common, app, anim_mult);
|
|
app.tasks
|
|
.enqueue(TaskType::Overlay(OverlayTask::Modify(sel, task)));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::EditModeToggleGrab" => Box::new(move |_common, data, app, state| {
|
|
if !test_button(data) || !test_duration(&button, app) {
|
|
return Ok(EventResult::Pass);
|
|
}
|
|
|
|
let sel = OverlaySelector::Id(*state.id.borrow());
|
|
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
|
sel,
|
|
Box::new(|_app, owc| {
|
|
let state = owc.active_state.as_mut().unwrap(); //want panic
|
|
state.grabbable = !state.grabbable;
|
|
}),
|
|
)));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
"::EditModeTab" => {
|
|
let tab_name = args.next().unwrap().to_owned();
|
|
Box::new(move |common, data, app, state| {
|
|
if !test_button(data) || !test_duration(&button, app) {
|
|
return Ok(EventResult::Pass);
|
|
}
|
|
|
|
state.tabs.tab_button_clicked(common, &tab_name);
|
|
Ok(EventResult::Consumed)
|
|
})
|
|
}
|
|
"::EditModeSetPos" => {
|
|
let key = args.next().unwrap().to_owned();
|
|
Box::new(move |common, data, app, state| {
|
|
if !test_button(data) || !test_duration(&button, app) {
|
|
return Ok(EventResult::Pass);
|
|
}
|
|
|
|
let sel = OverlaySelector::Id(*state.id.borrow());
|
|
let task = state.pos.button_clicked(common, &key);
|
|
app.tasks
|
|
.enqueue(TaskType::Overlay(OverlayTask::Modify(sel, task)));
|
|
Ok(EventResult::Consumed)
|
|
})
|
|
}
|
|
"::EditModeSetStereo" => {
|
|
let key = args.next().unwrap().to_owned();
|
|
Box::new(move |common, data, app, state| {
|
|
if !test_button(data) || !test_duration(&button, app) {
|
|
return Ok(EventResult::Pass);
|
|
}
|
|
|
|
let sel = OverlaySelector::Id(*state.id.borrow());
|
|
let task = state.stereo.button_clicked(common, &key);
|
|
app.tasks
|
|
.enqueue(TaskType::Overlay(OverlayTask::Modify(sel, task)));
|
|
Ok(EventResult::Consumed)
|
|
})
|
|
}
|
|
"::EditModeSetMouse" => {
|
|
let key = args.next().unwrap().to_owned();
|
|
Box::new(move |common, data, app, state| {
|
|
if !test_button(data) || !test_duration(&button, app) {
|
|
return Ok(EventResult::Pass);
|
|
}
|
|
|
|
let sel = OverlaySelector::Id(*state.id.borrow());
|
|
let task = state.mouse.button_clicked(common, &key);
|
|
app.tasks
|
|
.enqueue(TaskType::Overlay(OverlayTask::Modify(sel, task)));
|
|
Ok(EventResult::Consumed)
|
|
})
|
|
}
|
|
"::EditModeDelete" => Box::new(move |_common, data, app, state| {
|
|
if !test_button(data) || !test_duration(&button, app) {
|
|
return Ok(EventResult::Pass);
|
|
}
|
|
|
|
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
|
OverlaySelector::Id(*state.id.borrow()),
|
|
Box::new(move |_app, owc| {
|
|
owc.active_state = None;
|
|
}),
|
|
)));
|
|
Ok(EventResult::Consumed)
|
|
}),
|
|
_ => return,
|
|
};
|
|
|
|
let id = layout.add_event_listener(attribs.widget_id, *kind, callback);
|
|
log::debug!("Registered {action} on {:?} as {id:?}", attribs.widget_id);
|
|
}
|
|
});
|
|
|
|
let mut panel = GuiPanel::new_from_template(
|
|
app,
|
|
"gui/edit.xml",
|
|
state,
|
|
NewGuiPanelParams {
|
|
on_custom_attrib: Some(on_custom_attrib),
|
|
resize_to_parent: true,
|
|
..Default::default()
|
|
},
|
|
)?;
|
|
|
|
panel.state.pos = new_pos_tab_handler(&mut panel)?;
|
|
panel.state.stereo = new_stereo_tab_handler(&mut panel)?;
|
|
panel.state.mouse = new_mouse_tab_handler(&mut panel)?;
|
|
panel.state.lock = InteractLockHandler::new(&mut panel)?;
|
|
panel.state.tabs = ButtonPaneTabSwitcher::new(
|
|
&mut panel,
|
|
&["none", "pos", "alpha", "curve", "stereo", "mouse"],
|
|
)?;
|
|
|
|
set_up_checkbox(&mut panel, "additive_box", cb_assign_additive)?;
|
|
set_up_checkbox(&mut panel, "align_box", cb_assign_align)?;
|
|
set_up_slider(&mut panel, "lerp_slider", cb_assign_lerp)?;
|
|
set_up_slider(&mut panel, "alpha_slider", cb_assign_alpha)?;
|
|
set_up_slider(&mut panel, "curve_slider", cb_assign_curve)?;
|
|
|
|
Ok(panel)
|
|
}
|
|
|
|
fn reset_panel(
|
|
panel: &mut EditModeWrapPanel,
|
|
id: OverlayID,
|
|
owc: &mut OverlayWindowConfig,
|
|
) -> anyhow::Result<()> {
|
|
*panel.state.id.borrow_mut() = id;
|
|
let state = owc.active_state.as_mut().unwrap();
|
|
|
|
let mut alterables = EventAlterables::default();
|
|
let mut common = CallbackDataCommon {
|
|
alterables: &mut alterables,
|
|
state: &panel.layout.state,
|
|
};
|
|
|
|
let c = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentButton>("top_grab")?;
|
|
c.set_sticky_state(&mut common, !state.grabbable);
|
|
|
|
let c = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentSlider>("lerp_slider")?;
|
|
c.set_value(&mut common, state.positioning.get_lerp().unwrap_or(1.0));
|
|
|
|
let c = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentSlider>("alpha_slider")?;
|
|
c.set_value(&mut common, state.alpha);
|
|
|
|
let c = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentSlider>("curve_slider")?;
|
|
c.set_value(&mut common, state.curvature.unwrap_or(0.0));
|
|
|
|
let c = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentCheckbox>("additive_box")?;
|
|
c.set_checked(&mut common, state.additive);
|
|
|
|
let c = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentCheckbox>("align_box")?;
|
|
c.set_checked(&mut common, state.positioning.get_align().unwrap_or(false));
|
|
|
|
panel
|
|
.state
|
|
.pos
|
|
.reset(&mut common, &state.positioning.into());
|
|
panel.state.lock.reset(&mut common, state.interactable);
|
|
panel.state.tabs.reset(&mut common);
|
|
|
|
if let Some(stereo) = attrib_value!(
|
|
owc.backend.get_attrib(BackendAttrib::Stereo),
|
|
BackendAttribValue::Stereo
|
|
) {
|
|
panel
|
|
.state
|
|
.tabs
|
|
.set_tab_visible(&mut common, "stereo", true);
|
|
panel.state.stereo.reset(&mut common, &stereo);
|
|
} else {
|
|
panel
|
|
.state
|
|
.tabs
|
|
.set_tab_visible(&mut common, "stereo", false);
|
|
}
|
|
|
|
if let Some(mouse) = attrib_value!(
|
|
owc.backend.get_attrib(BackendAttrib::MouseTransform),
|
|
BackendAttribValue::MouseTransform
|
|
) {
|
|
panel.state.tabs.set_tab_visible(&mut common, "mouse", true);
|
|
panel.state.mouse.reset(&mut common, &mouse);
|
|
} else {
|
|
panel
|
|
.state
|
|
.tabs
|
|
.set_tab_visible(&mut common, "mouse", false);
|
|
}
|
|
|
|
panel.layout.process_alterables(alterables)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
const fn cb_assign_lerp(_app: &mut AppState, owc: &mut OverlayWindowConfig, lerp: f32) {
|
|
owc.dirty = true;
|
|
let active_state = owc.active_state.as_mut().unwrap();
|
|
active_state.positioning = active_state.positioning.with_lerp(lerp);
|
|
}
|
|
|
|
const fn cb_assign_alpha(_app: &mut AppState, owc: &mut OverlayWindowConfig, alpha: f32) {
|
|
owc.dirty = true;
|
|
owc.active_state.as_mut().unwrap().alpha = alpha;
|
|
}
|
|
|
|
fn cb_assign_curve(_app: &mut AppState, owc: &mut OverlayWindowConfig, curvature: f32) {
|
|
owc.dirty = true;
|
|
owc.active_state.as_mut().unwrap().curvature = if curvature < 0.005 {
|
|
None
|
|
} else {
|
|
Some(curvature)
|
|
};
|
|
}
|
|
|
|
const fn cb_assign_additive(_app: &mut AppState, owc: &mut OverlayWindowConfig, additive: bool) {
|
|
owc.dirty = true;
|
|
owc.active_state.as_mut().unwrap().additive = additive;
|
|
}
|
|
|
|
const fn cb_assign_align(_app: &mut AppState, owc: &mut OverlayWindowConfig, align: bool) {
|
|
owc.dirty = true;
|
|
let active_state = owc.active_state.as_mut().unwrap();
|
|
active_state.positioning = active_state.positioning.with_align(align);
|
|
}
|
|
|
|
fn set_up_slider(
|
|
panel: &mut EditModeWrapPanel,
|
|
id: &str,
|
|
callback: fn(&mut AppState, &mut OverlayWindowConfig, f32),
|
|
) -> anyhow::Result<()> {
|
|
let slider = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentSlider>(id)?;
|
|
let tasks = panel.state.tasks.clone();
|
|
let overlay_id = panel.state.id.clone();
|
|
slider.on_value_changed(Box::new(move |_common, e| {
|
|
let mut tasks = tasks.borrow_mut();
|
|
let e_value = e.value;
|
|
|
|
tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
|
OverlaySelector::Id(*overlay_id.borrow()),
|
|
Box::new(move |app, owc| callback(app, owc, e_value)),
|
|
)));
|
|
Ok(())
|
|
}));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn set_up_checkbox(
|
|
panel: &mut EditModeWrapPanel,
|
|
id: &str,
|
|
callback: fn(&mut AppState, &mut OverlayWindowConfig, bool),
|
|
) -> anyhow::Result<()> {
|
|
let checkbox = panel
|
|
.parser_state
|
|
.fetch_component_as::<ComponentCheckbox>(id)?;
|
|
let tasks = panel.state.tasks.clone();
|
|
let overlay_id = panel.state.id.clone();
|
|
checkbox.on_toggle(Box::new(move |_common, e| {
|
|
let mut tasks = tasks.borrow_mut();
|
|
let e_checked = e.checked;
|
|
|
|
tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
|
|
OverlaySelector::Id(*overlay_id.borrow()),
|
|
Box::new(move |app, owc| callback(app, owc, e_checked)),
|
|
)));
|
|
Ok(())
|
|
}));
|
|
|
|
Ok(())
|
|
}
|