From 350c9317495ac983e38bc11be294dc5a56e331c1 Mon Sep 17 00:00:00 2001
From: galister <22305755+galister@users.noreply.github.com>
Date: Wed, 12 Nov 2025 01:38:04 +0900
Subject: [PATCH] wip: edit mode overlay
---
wlx-overlay-s/src/assets/gui/adjust.xml | 44 ++++
wlx-overlay-s/src/backend/input.rs | 28 +--
wlx-overlay-s/src/gui/panel/mod.rs | 38 ++--
wlx-overlay-s/src/overlays/adjust.rs | 195 ++++++++++++++++++
wlx-overlay-s/src/overlays/anchor.rs | 2 +-
wlx-overlay-s/src/overlays/bar.rs | 2 +-
wlx-overlay-s/src/overlays/custom.rs | 2 +-
.../src/overlays/keyboard/builder.rs | 4 +-
wlx-overlay-s/src/overlays/mod.rs | 1 +
wlx-overlay-s/src/overlays/toast.rs | 2 +-
wlx-overlay-s/src/overlays/watch.rs | 1 +
wlx-overlay-s/src/windowing/backend.rs | 45 +++-
wlx-overlay-s/src/windowing/manager.rs | 20 +-
wlx-overlay-s/src/windowing/window.rs | 3 +
14 files changed, 349 insertions(+), 38 deletions(-)
create mode 100644 wlx-overlay-s/src/assets/gui/adjust.xml
create mode 100644 wlx-overlay-s/src/overlays/adjust.rs
diff --git a/wlx-overlay-s/src/assets/gui/adjust.xml b/wlx-overlay-s/src/assets/gui/adjust.xml
new file mode 100644
index 0000000..4845e57
--- /dev/null
+++ b/wlx-overlay-s/src/assets/gui/adjust.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wlx-overlay-s/src/backend/input.rs b/wlx-overlay-s/src/backend/input.rs
index dfccfdd..bd7eb2d 100644
--- a/wlx-overlay-s/src/backend/input.rs
+++ b/wlx-overlay-s/src/backend/input.rs
@@ -19,7 +19,7 @@ use super::task::{TaskContainer, TaskType};
#[derive(Clone, Default)]
pub struct HoverResult {
pub haptics: Option,
- /// If true, the laster shows at this position and no further raycasting will be done.
+ /// If true, the laser shows at this position and no further raycasting will be done.
pub consume: bool,
}
@@ -344,15 +344,18 @@ where
// focus change
if let Some(hovered_id) = hovered_id
&& hovered_id != hit.overlay
- && let Some(old_hovered) = overlays.mut_by_id(hovered_id)
{
- if old_hovered.primary_pointer.is_some_and(|i| i == idx) {
- old_hovered.primary_pointer = None;
+ if let Some(old_hovered) = overlays.mut_by_id(hovered_id) {
+ if old_hovered.primary_pointer.is_some_and(|i| i == idx) {
+ old_hovered.primary_pointer = None;
+ }
+ log::debug!("{} on_left (focus changed)", old_hovered.config.name);
+ old_hovered.config.backend.on_left(app, idx);
}
- log::debug!("{} on_left (focus changed)", old_hovered.config.name);
- old_hovered.config.backend.on_left(app, idx);
+ overlays.edit_overlay(hovered_id, false, app);
}
+ overlays.edit_overlay(hit.overlay, true, app);
let Some(hovered) = overlays.mut_by_id(hit.overlay) else {
log::warn!("Hit overlay {:?} does not exist", hit.overlay);
return (0.0, None); // no hit
@@ -424,11 +427,12 @@ fn handle_no_hit(
overlays: &mut OverlayWindowManager,
app: &mut AppState,
) {
- if let Some(hovered_id) = hovered_id
- && let Some(hovered) = overlays.mut_by_id(hovered_id)
- {
- log::debug!("{} on_left (no hit)", hovered.config.name);
- hovered.config.backend.on_left(app, pointer_idx);
+ if let Some(hovered_id) = hovered_id {
+ if let Some(hovered) = overlays.mut_by_id(hovered_id) {
+ log::debug!("{} on_left (no hit)", hovered.config.name);
+ hovered.config.backend.on_left(app, pointer_idx);
+ }
+ overlays.edit_overlay(hovered_id, false, app);
}
// in case click released while not aiming at anything
@@ -568,7 +572,7 @@ where
};
let result = overlay.config.backend.on_hover(app, &hit);
- if result.consume {
+ if result.consume || overlay.config.editing {
return (Some(hit), result.haptics);
}
}
diff --git a/wlx-overlay-s/src/gui/panel/mod.rs b/wlx-overlay-s/src/gui/panel/mod.rs
index e2c938d..e8e062b 100644
--- a/wlx-overlay-s/src/gui/panel/mod.rs
+++ b/wlx-overlay-s/src/gui/panel/mod.rs
@@ -12,6 +12,7 @@ use wgui::{
InternalStateChangeEvent, MouseButtonIndex, MouseDownEvent, MouseLeaveEvent,
MouseMotionEvent, MouseUpEvent, MouseWheelEvent,
},
+ gfx::cmd::WGfxClearMode,
layout::{Layout, LayoutParams, WidgetID},
parser::ParserState,
renderer_vk::context::Context as WguiContext,
@@ -32,8 +33,7 @@ mod button;
mod helper;
mod label;
-const MAX_SIZE: u32 = 2048;
-const MAX_SIZE_VEC2: Vec2 = vec2(MAX_SIZE as _, MAX_SIZE as _);
+const DEFAULT_MAX_SIZE: f32 = 2048.0;
const COLOR_ERR: drawing::Color = drawing::Color::new(1., 0., 1., 1.);
@@ -42,6 +42,7 @@ pub struct GuiPanel {
pub state: S,
pub timers: Vec,
pub parser_state: ParserState,
+ pub max_size: Vec2,
interaction_transform: Option,
context: WguiContext,
timestep: Timestep,
@@ -63,6 +64,7 @@ impl GuiPanel {
path: &str,
state: S,
on_custom_id: Option,
+ resize_to_parent: bool,
) -> anyhow::Result {
let custom_elems = Rc::new(RefCell::new(vec![]));
@@ -81,7 +83,7 @@ impl GuiPanel {
};
let (mut layout, mut parser_state) =
- wgui::parser::new_layout_from_assets(&doc_params, &LayoutParams::default())?;
+ wgui::parser::new_layout_from_assets(&doc_params, &LayoutParams { resize_to_parent })?;
if let Some(on_element_id) = on_custom_id {
let ids = parser_state.data.ids.clone(); // FIXME: copying all ids?
@@ -125,13 +127,14 @@ impl GuiPanel {
timestep,
state,
parser_state,
+ max_size: vec2(DEFAULT_MAX_SIZE as _, DEFAULT_MAX_SIZE as _),
timers: vec![],
interaction_transform: None,
})
}
- pub fn new_blank(app: &mut AppState, state: S) -> anyhow::Result {
- let layout = Layout::new(app.wgui_globals.clone(), &LayoutParams::default())?;
+ pub fn new_blank(app: &mut AppState, state: S, resize_to_parent: bool) -> anyhow::Result {
+ let layout = Layout::new(app.wgui_globals.clone(), &LayoutParams { resize_to_parent })?;
let context = WguiContext::new(&mut app.wgui_shared, 1.0)?;
let mut timestep = Timestep::new();
timestep.set_tps(60.0);
@@ -142,13 +145,14 @@ impl GuiPanel {
timestep,
state,
parser_state: ParserState::default(),
+ max_size: vec2(DEFAULT_MAX_SIZE as _, DEFAULT_MAX_SIZE as _),
timers: vec![],
interaction_transform: None,
})
}
pub fn update_layout(&mut self) -> anyhow::Result<()> {
- self.layout.update(MAX_SIZE_VEC2, 0.0)
+ self.layout.update(self.max_size, 0.0)
}
pub fn push_event(&mut self, app: &mut AppState, event: &WguiEvent) -> EventResult {
@@ -223,21 +227,25 @@ impl OverlayBackend for GuiPanel {
app: &mut AppState,
tgt: Arc,
buf: &mut CommandBuffers,
- alpha: f32,
+ mut alpha: f32,
) -> anyhow::Result {
self.context
.update_viewport(&mut app.wgui_shared, tgt.extent_u32arr(), 1.0)?;
- self.layout.update(MAX_SIZE_VEC2, self.timestep.alpha)?;
+ self.layout.update(self.max_size, self.timestep.alpha)?;
+
+ // FIXME: pass this properly
+ let mut clear = WGfxClearMode::Clear([0., 0., 0., 0.]);
+ if alpha < 0. {
+ alpha *= -1.;
+ clear = WGfxClearMode::Keep;
+ }
let mut cmd_buf = app
.gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)
.unwrap(); // want panic
- cmd_buf.begin_rendering(
- tgt,
- wgui::gfx::cmd::WGfxClearMode::Clear([0.0, 0.0, 0.0, 0.0]),
- )?;
+ cmd_buf.begin_rendering(tgt, clear)?;
let globals = self.layout.state.globals.clone(); // sorry
let mut globals = globals.get();
@@ -263,8 +271,8 @@ impl OverlayBackend for GuiPanel {
fn frame_meta(&mut self) -> Option {
Some(FrameMeta {
extent: [
- MAX_SIZE.min(self.layout.content_size.x as _),
- MAX_SIZE.min(self.layout.content_size.y as _),
+ 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()
@@ -301,7 +309,7 @@ impl OverlayBackend for GuiPanel {
}
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
- log::info!("panel: on left");
+ log::debug!("panel: on left");
let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer });
self.push_event(app, &e);
}
diff --git a/wlx-overlay-s/src/overlays/adjust.rs b/wlx-overlay-s/src/overlays/adjust.rs
new file mode 100644
index 0000000..e060c9c
--- /dev/null
+++ b/wlx-overlay-s/src/overlays/adjust.rs
@@ -0,0 +1,195 @@
+use std::{
+ any::Any,
+ mem::{self, ManuallyDrop},
+ sync::Arc,
+};
+
+use glam::vec2;
+use vulkano::image::{view::ImageView, ImageUsage};
+
+use crate::{
+ backend::input::HoverResult,
+ gui::panel::GuiPanel,
+ state::AppState,
+ windowing::{
+ backend::{DummyBackend, OverlayBackend, ShouldRender},
+ window::OverlayWindowConfig,
+ },
+};
+
+type EditModeWrapPanel = GuiPanel>;
+
+#[derive(Default)]
+pub struct EditModeManager {
+ panel_pool: Vec,
+}
+
+impl EditModeManager {
+ pub fn wrap_edit_mode(
+ &mut self,
+ owc: &mut OverlayWindowConfig,
+ app: &mut AppState,
+ ) -> anyhow::Result<()> {
+ if owc.editing {
+ return Ok(());
+ }
+
+ log::debug!("EditMode wrap on {}", owc.name);
+ let inner = mem::replace(&mut owc.backend, Box::new(DummyBackend {}));
+ let mut panel = self.panel_pool.pop();
+ if panel.is_none() {
+ panel = Some(make_adjustment_panel(app)?);
+ }
+ let mut panel = panel.unwrap();
+ panel.state = owc.name.clone();
+ owc.backend = Box::new(EditModeBackendWrapper {
+ inner: ManuallyDrop::new(inner),
+ panel: ManuallyDrop::new(panel),
+ can_render_inner: false,
+ image: None,
+ });
+ owc.editing = true;
+
+ Ok(())
+ }
+
+ pub fn unwrap_edit_mode(&mut self, owc: &mut OverlayWindowConfig) {
+ if !owc.editing {
+ return;
+ }
+
+ log::debug!("EditMode unwrap on {}", owc.name);
+ let wrapper = mem::replace(&mut owc.backend, Box::new(DummyBackend {}));
+ let mut wrapper: Box = wrapper;
+ let wrapper = wrapper
+ .downcast_mut::()
+ .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;
+
+ // wrapper is destroyed with nothing left inside
+ }
+}
+
+pub struct EditModeBackendWrapper {
+ panel: ManuallyDrop,
+ inner: ManuallyDrop>,
+ image: Option>,
+ 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 {
+ let i = self.inner.should_render(app)?;
+
+ self.can_render_inner = !matches!(i, ShouldRender::Unable);
+ if self.can_render_inner
+ && let Some(ref frame_meta) = self.inner.frame_meta()
+ {
+ let new_size = vec2(frame_meta.extent[0] as _, frame_meta.extent[1] as _);
+ if self.panel.max_size != new_size {
+ log::debug!("EditWrapperGui size {} → {new_size}", self.panel.max_size);
+ self.panel.max_size = new_size;
+ self.panel.update_layout()?;
+ }
+ }
+
+ let p = self.panel.should_render(app)?;
+
+ Ok(match (i, p) {
+ (ShouldRender::Should, ShouldRender::Should) => ShouldRender::Should,
+ (ShouldRender::Should, ShouldRender::Can) => ShouldRender::Should,
+ (ShouldRender::Can, ShouldRender::Should) => ShouldRender::Should,
+ (ShouldRender::Can, ShouldRender::Can) => ShouldRender::Can,
+ // (ShouldRender::Unable, ShouldRender::Should) if self.image.is_some() => {
+ // ShouldRender::Should
+ // }
+ // (ShouldRender::Unable, ShouldRender::Can) if self.image.is_some() => ShouldRender::Can,
+ _ => ShouldRender::Unable,
+ })
+ }
+ fn render(
+ &mut self,
+ app: &mut crate::state::AppState,
+ tgt: std::sync::Arc,
+ buf: &mut crate::graphics::CommandBuffers,
+ alpha: f32,
+ ) -> anyhow::Result {
+ if self.can_render_inner {
+ if self.image.is_none()
+ && let Some(ref meta) = self.inner.frame_meta()
+ {
+ let image = app.gfx.new_image(
+ meta.extent[0],
+ meta.extent[1],
+ app.gfx.surface_format,
+ ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
+ )?;
+ self.image = Some(ImageView::new_default(image)?);
+ }
+ self.inner.render(app, tgt.clone(), buf, alpha)?;
+ }
+
+ self.panel.render(app, tgt, buf, -1.)
+ }
+ fn frame_meta(&mut self) -> Option {
+ self.inner.frame_meta().or_else(|| self.panel.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_y: f32,
+ delta_x: f32,
+ ) {
+ self.panel.on_scroll(app, hit, delta_y, delta_x);
+ }
+ fn get_interaction_transform(&mut self) -> Option {
+ self.inner.get_interaction_transform()
+ }
+}
+
+fn make_adjustment_panel(app: &mut AppState) -> anyhow::Result {
+ let mut panel = GuiPanel::new_from_template(app, "gui/adjust.xml", "".into(), None, true)?;
+ panel.update_layout()?;
+
+ Ok(panel)
+}
diff --git a/wlx-overlay-s/src/overlays/anchor.rs b/wlx-overlay-s/src/overlays/anchor.rs
index ede4a13..90d0f98 100644
--- a/wlx-overlay-s/src/overlays/anchor.rs
+++ b/wlx-overlay-s/src/overlays/anchor.rs
@@ -9,7 +9,7 @@ use crate::windowing::Z_ORDER_ANCHOR;
pub static ANCHOR_NAME: LazyLock> = LazyLock::new(|| Arc::from("anchor"));
pub fn create_anchor(app: &mut AppState) -> anyhow::Result {
- let mut panel = GuiPanel::new_from_template(app, "gui/anchor.xml", (), None)?;
+ let mut panel = GuiPanel::new_from_template(app, "gui/anchor.xml", (), None, false)?;
panel.update_layout()?;
Ok(OverlayWindowConfig {
diff --git a/wlx-overlay-s/src/overlays/bar.rs b/wlx-overlay-s/src/overlays/bar.rs
index c13f963..fa61351 100644
--- a/wlx-overlay-s/src/overlays/bar.rs
+++ b/wlx-overlay-s/src/overlays/bar.rs
@@ -15,7 +15,7 @@ struct BarState {}
#[allow(clippy::match_same_arms)] // TODO: remove later
pub fn create_bar(app: &mut AppState) -> anyhow::Result {
let state = BarState {};
- let mut panel = GuiPanel::new_from_template(app, "gui/bar.xml", state, None)?;
+ let mut panel = GuiPanel::new_from_template(app, "gui/bar.xml", state, None, false)?;
for (id, _widget_id) in &panel.parser_state.data.ids {
match id.as_ref() {
diff --git a/wlx-overlay-s/src/overlays/custom.rs b/wlx-overlay-s/src/overlays/custom.rs
index 9cef94b..79bc183 100644
--- a/wlx-overlay-s/src/overlays/custom.rs
+++ b/wlx-overlay-s/src/overlays/custom.rs
@@ -18,7 +18,7 @@ pub fn create_custom(app: &mut AppState, name: Arc) -> Option Option {
toast.title
};
- let mut panel = GuiPanel::new_blank(app, ()).ok()?;
+ let mut panel = GuiPanel::new_blank(app, (), false).ok()?;
let globals = panel.layout.state.globals.clone();
diff --git a/wlx-overlay-s/src/overlays/watch.rs b/wlx-overlay-s/src/overlays/watch.rs
index bafde43..3c4f076 100644
--- a/wlx-overlay-s/src/overlays/watch.rs
+++ b/wlx-overlay-s/src/overlays/watch.rs
@@ -37,6 +37,7 @@ pub fn create_watch(app: &mut AppState, num_sets: usize) -> anyhow::Result anyhow::Result<()>;
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()>;
@@ -46,7 +46,7 @@ pub trait OverlayBackend {
/// Called to retrieve the effective extent of the image
/// Used for creating swapchains.
///
- /// Must be true if should_render was also true on the same frame.
+ /// Must be Some if should_render was Should or Can on the same frame.
fn frame_meta(&mut self) -> Option;
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult;
@@ -69,3 +69,42 @@ pub fn ui_transform(extent: [u32; 2]) -> Affine2 {
let center = Vec2 { x: 0.5, y: 0.5 };
Affine2::from_scale_angle_translation(scale, 0.0, center)
}
+
+pub struct DummyBackend {}
+
+impl OverlayBackend for DummyBackend {
+ fn init(&mut self, _: &mut AppState) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn pause(&mut self, _: &mut AppState) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn resume(&mut self, _: &mut AppState) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn should_render(&mut self, _: &mut AppState) -> anyhow::Result {
+ Ok(ShouldRender::Unable)
+ }
+ fn render(
+ &mut self,
+ _: &mut AppState,
+ _: Arc,
+ _: &mut CommandBuffers,
+ _: f32,
+ ) -> anyhow::Result {
+ Ok(false)
+ }
+ fn frame_meta(&mut self) -> Option {
+ None
+ }
+
+ fn on_hover(&mut self, _: &mut AppState, _: &PointerHit) -> HoverResult {
+ HoverResult::default()
+ }
+ fn on_left(&mut self, _: &mut AppState, _: usize) {}
+ fn on_pointer(&mut self, _: &mut AppState, _: &PointerHit, _: bool) {}
+ fn on_scroll(&mut self, _: &mut AppState, _: &PointerHit, _: f32, _: f32) {}
+ fn get_interaction_transform(&mut self) -> Option {
+ None
+ }
+}
diff --git a/wlx-overlay-s/src/windowing/manager.rs b/wlx-overlay-s/src/windowing/manager.rs
index b4a64eb..92309f4 100644
--- a/wlx-overlay-s/src/windowing/manager.rs
+++ b/wlx-overlay-s/src/windowing/manager.rs
@@ -5,8 +5,8 @@ use slotmap::{HopSlotMap, Key, SecondaryMap};
use crate::{
overlays::{
- anchor::create_anchor, keyboard::builder::create_keyboard, screen::create_screens,
- watch::create_watch,
+ adjust::EditModeManager, anchor::create_anchor, keyboard::builder::create_keyboard,
+ screen::create_screens, watch::create_watch,
},
state::AppState,
windowing::{
@@ -18,6 +18,7 @@ use crate::{
};
pub struct OverlayWindowManager {
+ wrappers: EditModeManager,
overlays: HopSlotMap>,
sets: Vec,
/// The set that is currently visible.
@@ -37,6 +38,7 @@ where
let mut maybe_keymap = None;
let mut me = Self {
+ wrappers: EditModeManager::default(),
overlays: HopSlotMap::with_key(),
current_set: Some(0),
restore_set: 0,
@@ -168,6 +170,20 @@ impl OverlayWindowManager {
self.restore_set = (app.session.config.last_set as usize).min(self.sets.len() - 1);
}
+ pub fn edit_overlay(&mut self, id: OverlayID, enabled: bool, app: &mut AppState) {
+ let Some(overlay) = self.overlays.get_mut(id) else {
+ return;
+ };
+
+ if enabled {
+ self.wrappers
+ .wrap_edit_mode(&mut overlay.config, app)
+ .unwrap(); // FIXME: unwrap
+ } else {
+ self.wrappers.unwrap_edit_mode(&mut overlay.config);
+ }
+ }
+
pub fn mut_by_selector(
&mut self,
selector: &OverlaySelector,
diff --git a/wlx-overlay-s/src/windowing/window.rs b/wlx-overlay-s/src/windowing/window.rs
index afc1f27..e52e1a4 100644
--- a/wlx-overlay-s/src/windowing/window.rs
+++ b/wlx-overlay-s/src/windowing/window.rs
@@ -99,6 +99,8 @@ pub struct OverlayWindowConfig {
pub global: bool,
/// True if transform, curvature, alpha has changed. Only used by OpenVR.
pub dirty: bool,
+ /// True if the window is showing the edit overlay
+ pub editing: bool,
pub saved_transform: Option,
}
@@ -118,6 +120,7 @@ impl OverlayWindowConfig {
show_on_spawn: false,
global: false,
dirty: true,
+ editing: false,
}
}