done: grabbable resize corner

This commit is contained in:
galister 2026-07-05 22:11:32 +09:00
parent 4f76a4abfb
commit 21749e22ed
15 changed files with 182 additions and 46 deletions

View File

@ -24,7 +24,7 @@
<sprite src_builtin="dashboard/welcome.svg" width="32" height="32" margin="16"/>
</Button>
<Button id="btn_website" macro="home_button" tooltip="wayvr.org">
<Button id="btn_website" macro="home_button" tooltip_str="wayvr.org">
<sprite src_builtin="dashboard/globe.svg" width="32" height="32" margin="16"/>
</Button>

View File

@ -34,7 +34,8 @@
<elements>
<div width="100%" height="100%">
<rectangle id="shadow" width="100%" height="100%" padding="4" gap="4" align_items="center" justify_content="center" color="#000000c0" flex_direction="row">
<rectangle id="shadow" width="100%" height="100%" padding="4" gap="4" align_items="center" justify_content="space_between" color="#000000c0" flex_direction="column">
<div />
<div flex_direction="column">
<rectangle padding="16" gap="8" round="32" color="~color_bg" border="2" border_color="~color_accent" justify_content="center">
<div flex_direction="column" gap="8">
@ -126,9 +127,13 @@
</div>
</rectangle>
</div>
<Button id="resize_corner" macro="button_style" tooltip="EDIT_MODE.RESIZE" _press="::EditModeResizeStart" _release="::EditModeResizeStop" border_color="~color_accent_translucent" color="~color_accent_50" color2="~color_accent_10">
<sprite width="48" height="48" src="edit/resize-corner.svg" />
</Button>
<div width="100%" justify_content="end" flex_direction="row">
<div>
<Button id="resize_corner" macro="button_style" tooltip="EDIT_MODE.RESIZE_CORNER" tooltip_side="left" _press="::EditModeResizeStart" _release="::EditModeResizeStop" border_color="~color_accent_translucent" color="~color_accent_50" color2="~color_accent_10">
<sprite width="48" height="48" src="edit/resize-corner.svg" />
</Button>
</div>
</div>
</rectangle>
</div>
</elements>

View File

@ -64,6 +64,7 @@
"POS_STATIC": "Static: Not part of any set, no recenter.",
"POSITIONING": "Positioning",
"RESIZE_PRESS_AND_DRAG": "Resize (press & drag)",
"RESIZE_CORNER": "Click & hold to resize",
"STEREO_3D_MODE": {
"ADJUST_MOUSE": "Adjust mouse",
"FULL_FRAME": "Full 3D",

View File

@ -735,7 +735,9 @@ where
};
if uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 {
continue;
if !overlay.config.resizing {
continue;
}
}
let pointer_hit = PointerHit {

View File

@ -110,6 +110,7 @@ pub enum OverlayTask {
SwitchSet(Option<usize>),
ToggleOverlay(OverlaySelector, ToggleMode),
ResetOverlay(OverlaySelector),
ResizeOverlay(OverlaySelector, [u32; 2]),
DeleteActiveSet,
ToggleEditMode,
ToggleDashboard,

View File

@ -392,6 +392,7 @@ impl WvrServerState {
let window_handle = wvr_server.wm.create_window(
toplevel.clone(),
process_handle,
output_bounds,
min_size,
max_size,
fallback_size.w as _,

View File

@ -10,6 +10,7 @@ use crate::{backend::wayvr::process, gen_id};
pub struct Window {
pub min_size: Size<i32, Logical>,
pub max_size: Size<i32, Logical>,
pub bounds: Size<i32, Logical>,
pub size_x: u32,
pub size_y: u32,
pub visible: bool,
@ -21,10 +22,12 @@ impl Window {
const fn new(
toplevel: Rc<ToplevelSurface>,
process: process::ProcessHandle,
bounds: Size<i32, Logical>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
) -> Self {
Self {
bounds,
min_size,
max_size,
size_x: 0,
@ -39,6 +42,17 @@ impl Window {
self.min_size != self.max_size
}
pub fn checked_configure_size(&mut self, size: Size<i32, Logical>) {
let clamped_size = size.clamp(self.min_size, self.max_size);
self.toplevel.with_pending_state(|state| {
state.bounds = Some(self.bounds);
state.size = Some(clamped_size);
});
self.toplevel.send_configure();
self.remember_committed_size(size);
}
pub fn configure_size(&mut self, size: Option<Size<i32, Logical>>, bounds: Size<i32, Logical>) {
self.toplevel.with_pending_state(|state| {
state.bounds = Some(bounds);
@ -46,6 +60,7 @@ impl Window {
});
self.toplevel.send_configure();
self.bounds = bounds;
if let Some(size) = size {
self.remember_committed_size(size);
}
@ -106,12 +121,13 @@ impl WindowManager {
&mut self,
toplevel: Rc<ToplevelSurface>,
process: process::ProcessHandle,
bounds: Size<i32, Logical>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
size_x: u32,
size_y: u32,
) -> WindowHandle {
let mut window = Window::new(toplevel, process, min_size, max_size);
let mut window = Window::new(toplevel, process, bounds, min_size, max_size);
window.remember_committed_size(Size::new(size_x as i32, size_y as i32));
self.windows.add(window)
}

View File

@ -367,7 +367,7 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
let e = WguiEvent::MouseWheel(MouseWheelEvent {
delta: vec2(delta.x, delta.y) / 8.0,
pos: hit.uv * self.layout.content_size,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.push_event(app, &e);
}
@ -375,7 +375,7 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult {
let e = &WguiEvent::MouseMotion(MouseMotionEvent {
pos: hit.uv * self.layout.content_size,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.has_focus[hit.pointer] = true;
@ -397,7 +397,7 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
device: DeviceBitmask::from_usize(pointer),
device: DeviceBitmask::from_index(pointer),
});
self.has_focus[pointer] = false;
self.push_event(app, &e);
@ -415,13 +415,13 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
WguiEvent::MouseDown(MouseButtonEvent {
pos: hit.uv * self.layout.content_size,
index,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
})
} else {
WguiEvent::MouseUp(MouseButtonEvent {
pos: hit.uv * self.layout.content_size,
index,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
})
};
self.push_event(app, &e);
@ -430,11 +430,11 @@ impl<S: 'static> OverlayBackend for GuiPanel<S> {
if !pressed && !self.has_focus[hit.pointer] {
let e = WguiEvent::MouseMotion(MouseMotionEvent {
pos: vec2(-1., -1.),
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.push_event(app, &e);
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.push_event(app, &e);
}

View File

@ -233,7 +233,7 @@ impl OverlayBackend for DashFrontend {
let e = WguiEvent::MouseWheel(MouseWheelEvent {
delta: vec2(delta.x, delta.y) / 8.0,
pos: hit.uv * self.inner.layout.content_size,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.push_event(&e);
}
@ -241,7 +241,7 @@ impl OverlayBackend for DashFrontend {
fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> HoverResult {
let e = &WguiEvent::MouseMotion(MouseMotionEvent {
pos: hit.uv * self.inner.layout.content_size,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.has_focus[hit.pointer] = true;
@ -264,7 +264,7 @@ impl OverlayBackend for DashFrontend {
fn on_left(&mut self, _app: &mut AppState, pointer: usize) {
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
device: DeviceBitmask::from_usize(pointer),
device: DeviceBitmask::from_index(pointer),
});
self.has_focus[pointer] = false;
self.push_event(&e);
@ -282,13 +282,13 @@ impl OverlayBackend for DashFrontend {
WguiEvent::MouseDown(MouseButtonEvent {
pos: hit.uv * self.inner.layout.content_size,
index,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
})
} else {
WguiEvent::MouseUp(MouseButtonEvent {
pos: hit.uv * self.inner.layout.content_size,
index,
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
})
};
self.push_event(&e);
@ -297,11 +297,11 @@ impl OverlayBackend for DashFrontend {
if !pressed && !self.has_focus[hit.pointer] {
let e = WguiEvent::MouseMotion(MouseMotionEvent {
pos: vec2(-1., -1.),
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.push_event(&e);
let e = WguiEvent::MouseLeave(MouseLeaveEvent {
device: DeviceBitmask::from_usize(hit.pointer),
device: DeviceBitmask::from_index(hit.pointer),
});
self.push_event(&e);
}

View File

@ -52,6 +52,19 @@ mod sprite_tab;
mod stereo;
pub mod tab;
enum ResizeState {
None,
Start {
overlay_id: OverlayID,
},
Active {
overlay_id: OverlayID,
pointer: usize,
start_uv: Vec2,
last_uv: Vec2,
},
}
struct EditModeState {
tasks: Rc<RefCell<TaskContainer>>,
id: Rc<RefCell<OverlayID>>,
@ -60,7 +73,23 @@ struct EditModeState {
pos: SpriteTabHandler<PosTabState>,
stereo: SpriteTabHandler<StereoMode>,
mouse: SpriteTabHandler<MouseTransform>,
resize_uv: Option<Vec2>,
resize: ResizeState,
}
impl EditModeState {
fn resize_end(&mut self, app: &mut AppState) {
match std::mem::replace(&mut self.resize, ResizeState::None) {
ResizeState::Active { overlay_id, .. } => {
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Id(overlay_id),
Box::new(|_app, owc| {
owc.resizing = false;
}),
)));
}
_ => {}
}
}
}
type EditModeWrapPanel = GuiPanel<EditModeState>;
@ -96,7 +125,7 @@ impl EditWrapperManager {
owc.backend = Box::new(EditModeBackendWrapper {
inner: ManuallyDrop::new(inner),
panel: ManuallyDrop::new(panel),
last_extent: [0, 0],
extent: [0, 0],
last_render: Instant::now(),
can_render_inner: false,
});
@ -138,7 +167,7 @@ pub struct EditModeBackendWrapper {
panel: ManuallyDrop<EditModeWrapPanel>,
inner: ManuallyDrop<Box<dyn OverlayBackend>>,
last_extent: [u32; 2],
extent: [u32; 2],
last_render: Instant,
can_render_inner: bool,
}
@ -217,7 +246,7 @@ impl OverlayBackend for EditModeBackendWrapper {
rdr.cmd_bufs.reverse();
}
self.last_extent = rdr.extent;
self.extent = rdr.extent;
Ok(())
}
@ -233,23 +262,32 @@ impl OverlayBackend for EditModeBackendWrapper {
let _ = self.inner.on_hover(app, hit);
let res = self.panel.on_hover(app, hit);
if let Some(start_uv) = self.panel.state.resize_uv.as_mut() {
let extent_f32 = vec2(self.last_extent[0] as f32, self.last_extent[1] as f32);
match &mut self.panel.state.resize {
ResizeState::Active {
overlay_id,
pointer,
start_uv,
last_uv,
} if *pointer == hit.pointer => {
// freshest extent we can get is from last frame, so use uv from that frame
let new_extent = resize_centered_from_uv(*start_uv, *last_uv, self.extent);
if start_uv.length_squared() > 10.0 {
// at first this is raw position in pixels, transform to UV here
*start_uv /= extent_f32;
} else {
// actually do resize
let new_res = extent_f32 * hit.uv / *start_uv;
let new_extent = [new_res.x as u32, new_res.y as u32];
log::warn!("NEW SIZE: {new_extent:?}");
app.tasks
.enqueue(TaskType::Overlay(OverlayTask::ResizeOverlay(
OverlaySelector::Id(*overlay_id),
new_extent,
)));
*last_uv = hit.uv;
}
_ => {}
}
res
}
fn on_left(&mut self, app: &mut crate::state::AppState, pointer: usize) {
self.panel.state.resize_end(app);
self.inner.on_left(app, pointer);
self.panel.on_left(app, pointer);
}
@ -260,6 +298,26 @@ impl OverlayBackend for EditModeBackendWrapper {
pressed: bool,
) {
self.panel.on_pointer(app, hit, pressed);
match &mut self.panel.state.resize {
ResizeState::Start { overlay_id } => {
app.tasks.enqueue(TaskType::Overlay(OverlayTask::Modify(
OverlaySelector::Id(*overlay_id),
Box::new(|_app, owc| {
owc.resizing = true;
}),
)));
let last_uv = hit.uv;
self.panel.state.resize = ResizeState::Active {
overlay_id: *overlay_id,
pointer: hit.pointer,
start_uv: last_uv,
last_uv: last_uv,
};
}
_ => {}
}
}
fn on_scroll(
&mut self,
@ -270,7 +328,8 @@ impl OverlayBackend for EditModeBackendWrapper {
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)
self.panel.notify(app, event_data.clone())?;
self.inner.notify(app, event_data)
}
fn get_interaction_transform(&mut self) -> Option<glam::Affine2> {
self.inner.get_interaction_transform()
@ -293,7 +352,7 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result<EditModeWrapPanel> {
pos: SpriteTabHandler::default(),
stereo: SpriteTabHandler::default(),
mouse: SpriteTabHandler::default(),
resize_uv: None,
resize: ResizeState::None,
};
let anim_mult = app.wgui_theme.animation_mult;
@ -434,14 +493,16 @@ fn make_edit_panel(app: &mut AppState) -> anyhow::Result<EditModeWrapPanel> {
if !test_button(data) || !test_duration(&button, app) {
return Ok(EventResult::Pass);
}
state.resize_uv = data.metadata.get_mouse_pos_absolute();
state.resize = ResizeState::Start {
overlay_id: state.id.borrow().clone(),
};
Ok(EventResult::Consumed)
}),
"::EditModeResizeStop" => Box::new(move |_common, data, app, state| {
if !test_button(data) || !test_duration(&button, app) {
return Ok(EventResult::Pass);
}
state.resize_uv = None;
state.resize_end(app);
Ok(EventResult::Consumed)
}),
_ => return,
@ -742,3 +803,25 @@ fn set_up_checkbox(
Ok(())
}
pub fn resize_centered_from_uv(grab_uv: Vec2, cur_uv: Vec2, cur_extent: [u32; 2]) -> [u32; 2] {
const MIN_SIZE: Vec2 = Vec2::new(160.0, 160.0);
const CENTER_DEADZONE: f32 = 0.1;
let cur_extent_v = Vec2::new(cur_extent[0] as f32, cur_extent[1] as f32);
let grab_from_center = grab_uv - Vec2::splat(0.5);
let cur_from_center = (cur_uv - Vec2::splat(0.5)) * cur_extent_v;
let mut out = cur_extent_v;
if grab_from_center.x.abs() > CENTER_DEADZONE {
out.x = (cur_from_center.x / grab_from_center.x).abs();
}
if grab_from_center.y.abs() > CENTER_DEADZONE {
out.y = (cur_from_center.y / grab_from_center.y).abs();
}
out = out.max(MIN_SIZE);
[out.x.round() as u32, out.y.round() as u32]
}

View File

@ -62,6 +62,7 @@ use crate::{
},
};
#[derive(Clone)]
pub enum WvrCommand {
CloseWindow,
KillProcess(KillSignal),
@ -800,6 +801,15 @@ impl OverlayBackend for WvrWindowBackend {
};
wvr_server.terminate_process(p.process, signal);
}
OverlayEventData::ResizeRequest(new_size) => {
let wvr_server = app.wvr_server.as_mut().unwrap();
let Some(win) = wvr_server.wm.windows.get_mut(&self.window) else {
log::warn!("Could not process resize request: window not found");
return Ok(());
};
let size: Size<i32, Logical> = Size::new(new_size[0] as i32, new_size[1] as i32);
win.checked_configure_size(size);
}
_ => {}
}

View File

@ -108,6 +108,7 @@ pub struct OverlayMeta {
}
#[allow(clippy::enum_variant_names)]
#[derive(Clone)]
pub enum OverlayEventData {
/// Notifies a newly added overlay of its ID, even before the overlay is shown.
IdAssigned(OverlayID),
@ -128,6 +129,7 @@ pub enum OverlayEventData {
command: ModifyPanelCommand,
},
WvrCommand(WvrCommand),
ResizeRequest([u32; 2]),
}
pub trait OverlayBackend: Any {

View File

@ -193,6 +193,18 @@ where
}
}
}
OverlayTask::ResizeOverlay(sel, size) => {
let Some(id) = self.id_by_selector(&sel) else {
log::warn!("Overlay not found for task: {sel:?}");
return Ok(());
};
let o = &mut self.overlays[id];
o.config
.backend
.notify(app, OverlayEventData::ResizeRequest(size))?;
}
OverlayTask::ToggleOverlay(sel, mode) => {
let Some(id) = self.id_by_selector(&sel) else {
log::warn!("Overlay not found for task: {sel:?}");
@ -230,8 +242,6 @@ where
o.config.activate(app);
}
self.visible_overlays_changed(app)?;
return Ok(());
}
OverlayTask::ToggleEditMode => {
self.set_edit_mode(!self.edit_mode, app)?;

View File

@ -87,6 +87,8 @@ pub struct OverlayWindowConfig {
pub dirty: bool,
/// True if the window is showing the edit overlay
pub editing: bool,
/// True if the window is being resized. Captures pointer hits even if not directly hit.
pub resizing: bool,
/// Used by grab to pause following of HMD or other devices
pub pause_movement: bool,
}
@ -108,6 +110,7 @@ impl OverlayWindowConfig {
global: false,
dirty: true,
editing: false,
resizing: false,
pause_movement: false,
}
}

View File

@ -23,11 +23,13 @@ use crate::{
pub struct DeviceBitmask(pub u8);
impl DeviceBitmask {
pub fn from_usize(mask: usize) -> Self {
// more than 8 input devices?
debug_assert!(mask & !0xff == 0);
Self(mask as u8)
}
pub fn from_index(index: usize) -> Self {
debug_assert!(index < 8);
Self(1u8 << index)
}
pub fn to_index(self) -> Option<usize> {
(self.0 != 0).then(|| self.0.trailing_zeros() as usize)
}
}
#[derive(Debug, Clone, Copy)]