Add window drag move and drag resize without decoration example. (#15814)

# Objective

Add an example for the new drag move and drag resize introduced by PR
#15674 and fix #15734.

## Solution

I created an example that allows the user to exercise drag move and drag
resize separately. The user can also choose what direction the resize
works in.

![Screenshot 2024-10-10 at 4 06
43 AM](https://github.com/user-attachments/assets/1da558ab-a80f-49af-8b7d-bb635b0f038f)

### Name

The example is called `window_drag_move`. Happy to have that
bikeshedded.

### Contentious Refactor?

This PR removed the `ResizeDirection` enumeration in favor of using
`CompassOctant` which had the same variants. Perhaps this is
contentious.

### Unsafe?

In PR #15674 I mentioned that `start_drag_move()` and
`start_drag_resize()`'s requirement to only be called in the presence of
a left-click looks like a compiler-unenforceable contract that can cause
intermittent panics when not observed, so perhaps the functions should
be marked them unsafe. **I have not made that change** here since I
didn't see a clear consensus on that.

## Testing

I exercised this on x86 macOS. However, winit for macOS does not support
drag resize. It reports a good error when `start_drag_resize()` is
called. I'd like to see it tested on Windows and Linux.

---

## Showcase

Example window_drag_move shows how to drag or resize a window without
decoration.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Shane Celis 2024-10-15 16:38:35 -07:00 committed by GitHub
parent a09104b62c
commit 5157fef84b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 172 additions and 43 deletions

View file

@ -3286,6 +3286,17 @@ description = "Demonstrates customizing default window settings"
category = "Window"
wasm = true
[[example]]
name = "window_drag_move"
path = "examples/window/window_drag_move.rs"
doc-scrape-examples = true
[package.metadata.example.window_drag_move]
name = "Window Drag Move"
description = "Demonstrates drag move and drag resize without window decoration"
category = "Window"
wasm = false
[[example]]
name = "ambiguity_detection"
path = "tests/ecs/ambiguity_detection.rs"

View file

@ -4,7 +4,7 @@ use bevy_ecs::{
entity::{Entity, VisitEntities, VisitEntitiesMut},
prelude::{Component, ReflectComponent},
};
use bevy_math::{DVec2, IVec2, UVec2, Vec2};
use bevy_math::{CompassOctant, DVec2, IVec2, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
#[cfg(feature = "serialize")]
@ -460,7 +460,7 @@ impl Window {
///
/// There is no guarantee that this will work unless the left mouse button was
/// pressed immediately before this function was called.
pub fn start_drag_resize(&mut self, direction: ResizeDirection) {
pub fn start_drag_resize(&mut self, direction: CompassOctant) {
self.internal.drag_resize_request = Some(direction);
}
@ -994,32 +994,6 @@ pub enum CursorGrabMode {
Locked,
}
/// Defines the orientation in which a window resize will be performed.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Reflect)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum ResizeDirection {
/// Resize the window to the west.
West,
/// Resize the window to the north.
North,
/// Resize the window to the east.
East,
/// Resize the window to the south.
South,
/// Resize the window to the northwest.
Northwest,
/// Resize the window to the northeast.
Northeast,
/// Resize the window to the southwest.
Southwest,
/// Resize the window to the southeast.
Southeast,
}
/// Stores internal [`Window`] state that isn't directly accessible.
#[derive(Default, Debug, Copy, Clone, PartialEq, Reflect)]
#[cfg_attr(
@ -1036,7 +1010,7 @@ pub struct InternalWindowState {
/// If this is true then next frame we will ask to drag-move the window.
drag_move_request: bool,
/// If this is `Some` then the next frame we will ask to drag-resize the window.
drag_resize_request: Option<ResizeDirection>,
drag_resize_request: Option<CompassOctant>,
/// Unscaled cursor position.
physical_cursor_position: Option<DVec2>,
}
@ -1058,7 +1032,7 @@ impl InternalWindowState {
}
/// Consumes the current resize request, if it exists. This should only be called by window backends.
pub fn take_resize_request(&mut self) -> Option<ResizeDirection> {
pub fn take_resize_request(&mut self) -> Option<CompassOctant> {
self.drag_resize_request.take()
}
}

View file

@ -5,10 +5,10 @@ use bevy_input::{
touch::{ForceTouch, TouchInput, TouchPhase},
ButtonState,
};
use bevy_math::Vec2;
use bevy_math::{CompassOctant, Vec2};
#[cfg(feature = "custom_cursor")]
use bevy_window::SystemCursorIcon;
use bevy_window::{EnabledButtons, ResizeDirection, WindowLevel, WindowTheme};
use bevy_window::{EnabledButtons, WindowLevel, WindowTheme};
use winit::keyboard::{Key, NamedKey, NativeKey};
pub fn convert_keyboard_input(
@ -707,17 +707,15 @@ pub fn convert_enabled_buttons(enabled_buttons: EnabledButtons) -> winit::window
window_buttons
}
pub fn convert_resize_direction(
resize_direction: ResizeDirection,
) -> winit::window::ResizeDirection {
pub fn convert_resize_direction(resize_direction: CompassOctant) -> winit::window::ResizeDirection {
match resize_direction {
ResizeDirection::West => winit::window::ResizeDirection::West,
ResizeDirection::North => winit::window::ResizeDirection::North,
ResizeDirection::East => winit::window::ResizeDirection::East,
ResizeDirection::South => winit::window::ResizeDirection::South,
ResizeDirection::Northwest => winit::window::ResizeDirection::NorthWest,
ResizeDirection::Northeast => winit::window::ResizeDirection::NorthEast,
ResizeDirection::Southwest => winit::window::ResizeDirection::SouthWest,
ResizeDirection::Southeast => winit::window::ResizeDirection::SouthEast,
CompassOctant::West => winit::window::ResizeDirection::West,
CompassOctant::North => winit::window::ResizeDirection::North,
CompassOctant::East => winit::window::ResizeDirection::East,
CompassOctant::South => winit::window::ResizeDirection::South,
CompassOctant::NorthWest => winit::window::ResizeDirection::NorthWest,
CompassOctant::NorthEast => winit::window::ResizeDirection::NorthEast,
CompassOctant::SouthWest => winit::window::ResizeDirection::SouthWest,
CompassOctant::SouthEast => winit::window::ResizeDirection::SouthEast,
}
}

View file

@ -538,6 +538,7 @@ Example | Description
[Scale Factor Override](../examples/window/scale_factor_override.rs) | Illustrates how to customize the default window settings
[Screenshot](../examples/window/screenshot.rs) | Shows how to save screenshots to disk
[Transparent Window](../examples/window/transparent_window.rs) | Illustrates making the window transparent and hiding the window decoration
[Window Drag Move](../examples/window/window_drag_move.rs) | Demonstrates drag move and drag resize without window decoration
[Window Resizing](../examples/window/window_resizing.rs) | Demonstrates resizing and responding to resizing a window
[Window Settings](../examples/window/window_settings.rs) | Demonstrates customizing default window settings

View file

@ -0,0 +1,145 @@
//! This example illustrates drag move and drag resize without window
//! decorations.
//!
//! When window decorations are not present, the user cannot drag a window by
//! its titlebar to change its position. The `start_drag_move()` function
//! permits a users to drag a window by left clicking anywhere in the window;
//! left click must be pressed and other constraints can be imposed. For
//! instance an application could require a user to hold down alt and left click
//! to drag a window.
//!
//! The `start_drag_resize()` function behaves similarly but permits a window to
//! be resized.
use bevy::{math::CompassOctant, prelude::*};
/// Determine what do on left click.
#[derive(Resource, Debug)]
enum LeftClickAction {
/// Do nothing.
Nothing,
/// Move the window on left click.
Move,
/// Resize the window on left click.
Resize,
}
/// What direction index should the window resize toward.
#[derive(Resource)]
struct ResizeDir(usize);
/// Directions that the drag resizes the window toward.
const DIRECTIONS: [CompassOctant; 8] = [
CompassOctant::North,
CompassOctant::NorthEast,
CompassOctant::East,
CompassOctant::SouthEast,
CompassOctant::South,
CompassOctant::SouthWest,
CompassOctant::West,
CompassOctant::NorthWest,
];
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
decorations: false,
..default()
}),
..default()
}))
.insert_resource(ResizeDir(7))
.insert_resource(LeftClickAction::Move)
.add_systems(Startup, setup)
.add_systems(Update, (handle_input, move_or_resize_windows))
.run();
}
fn setup(mut commands: Commands) {
// Camera
commands.spawn(Camera3d::default());
// UI
commands
.spawn((
NodeBundle {
style: Style {
position_type: PositionType::Absolute,
padding: UiRect::all(Val::Px(5.0)),
..default()
},
background_color: Color::BLACK.with_alpha(0.75).into(),
..default()
},
GlobalZIndex(i32::MAX),
))
.with_children(|p| {
p.spawn(Text::default()).with_children(|p| {
p.spawn(TextSpan::new(
"Demonstrate drag move and drag resize without window decorations.\n\n",
));
p.spawn(TextSpan::new("Controls:\n"));
p.spawn(TextSpan::new("A - change left click action ["));
p.spawn(TextSpan::new("Move"));
p.spawn(TextSpan::new("]\n"));
p.spawn(TextSpan::new("S / D - change resize direction ["));
p.spawn(TextSpan::new("NorthWest"));
p.spawn(TextSpan::new("]\n"));
});
});
}
fn handle_input(
input: Res<ButtonInput<KeyCode>>,
mut action: ResMut<LeftClickAction>,
mut dir: ResMut<ResizeDir>,
example_text: Query<Entity, With<Text>>,
mut writer: TextUiWriter,
) {
use LeftClickAction::*;
if input.just_pressed(KeyCode::KeyA) {
*action = match *action {
Move => Resize,
Resize => Nothing,
Nothing => Move,
};
*writer.text(example_text.single(), 4) = format!("{:?}", *action);
}
if input.just_pressed(KeyCode::KeyS) {
dir.0 = dir
.0
.checked_sub(1)
.unwrap_or(DIRECTIONS.len().saturating_sub(1));
*writer.text(example_text.single(), 7) = format!("{:?}", DIRECTIONS[dir.0]);
}
if input.just_pressed(KeyCode::KeyD) {
dir.0 = (dir.0 + 1) % DIRECTIONS.len();
*writer.text(example_text.single(), 7) = format!("{:?}", DIRECTIONS[dir.0]);
}
}
fn move_or_resize_windows(
mut windows: Query<&mut Window>,
action: Res<LeftClickAction>,
input: Res<ButtonInput<MouseButton>>,
dir: Res<ResizeDir>,
) {
// Both `start_drag_move()` and `start_drag_resize()` must be called after a
// left mouse button press as done here.
//
// winit 0.30.5 may panic when initiated without a left mouse button press.
if input.just_pressed(MouseButton::Left) {
for mut window in windows.iter_mut() {
match *action {
LeftClickAction::Nothing => (),
LeftClickAction::Move => window.start_drag_move(),
LeftClickAction::Resize => {
let d = DIRECTIONS[dir.0];
window.start_drag_resize(d);
}
}
}
}
}